mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-04 04:06:06 -05:00
All emoji graphemes are double-width. Foot doesn’t support non-latin scripts. Ergo, this should result in the Right Thing, even though we’re not doing it the Right Way. Note that we’re now breaking cursor synchronization with nearly all applications. But the way I see it, the applications need to be updated.
1246 lines
46 KiB
C
1246 lines
46 KiB
C
#include "vt.h"
|
||
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
|
||
#if defined(FOOT_GRAPHEME_CLUSTERING)
|
||
#include <utf8proc.h>
|
||
#endif
|
||
|
||
#define LOG_MODULE "vt"
|
||
#define LOG_ENABLE_DBG 0
|
||
#include "log.h"
|
||
#include "config.h"
|
||
#include "csi.h"
|
||
#include "dcs.h"
|
||
#include "debug.h"
|
||
#include "grid.h"
|
||
#include "osc.h"
|
||
#include "util.h"
|
||
#include "xmalloc.h"
|
||
|
||
#define UNHANDLED() LOG_DBG("unhandled: %s", esc_as_string(term, final))
|
||
|
||
/* https://vt100.net/emu/dec_ansi_parser */
|
||
|
||
enum state {
|
||
STATE_GROUND,
|
||
STATE_ESCAPE,
|
||
STATE_ESCAPE_INTERMEDIATE,
|
||
|
||
STATE_CSI_ENTRY,
|
||
STATE_CSI_PARAM,
|
||
STATE_CSI_INTERMEDIATE,
|
||
STATE_CSI_IGNORE,
|
||
|
||
STATE_OSC_STRING,
|
||
|
||
STATE_DCS_ENTRY,
|
||
STATE_DCS_PARAM,
|
||
STATE_DCS_INTERMEDIATE,
|
||
STATE_DCS_IGNORE,
|
||
STATE_DCS_PASSTHROUGH,
|
||
|
||
STATE_SOS_PM_APC_STRING,
|
||
|
||
STATE_UTF8_21,
|
||
STATE_UTF8_31,
|
||
STATE_UTF8_32,
|
||
STATE_UTF8_41,
|
||
STATE_UTF8_42,
|
||
STATE_UTF8_43,
|
||
};
|
||
|
||
#if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG && 0
|
||
static const char *const state_names[] = {
|
||
[STATE_GROUND] = "ground",
|
||
|
||
[STATE_ESCAPE] = "escape",
|
||
[STATE_ESCAPE_INTERMEDIATE] = "escape intermediate",
|
||
|
||
[STATE_CSI_ENTRY] = "CSI entry",
|
||
[STATE_CSI_PARAM] = "CSI param",
|
||
[STATE_CSI_INTERMEDIATE] = "CSI intermediate",
|
||
[STATE_CSI_IGNORE] = "CSI ignore",
|
||
|
||
[STATE_OSC_STRING] = "OSC string",
|
||
|
||
[STATE_DCS_ENTRY] = "DCS entry",
|
||
[STATE_DCS_PARAM] = "DCS param",
|
||
[STATE_DCS_INTERMEDIATE] = "DCS intermediate",
|
||
[STATE_DCS_IGNORE] = "DCS ignore",
|
||
[STATE_DCS_PASSTHROUGH] = "DCS passthrough",
|
||
|
||
[STATE_SOS_PM_APC_STRING] = "sos/pm/apc string",
|
||
|
||
[STATE_UTF8_21] = "UTF8 2-byte 1/2",
|
||
[STATE_UTF8_31] = "UTF8 3-byte 1/3",
|
||
[STATE_UTF8_32] = "UTF8 3-byte 2/3",
|
||
};
|
||
#endif
|
||
|
||
#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
|
||
static const char *
|
||
esc_as_string(struct terminal *term, uint8_t final)
|
||
{
|
||
static char msg[1024];
|
||
int c = snprintf(msg, sizeof(msg), "\\E");
|
||
|
||
for (size_t i = 0; i < sizeof(term->vt.private); i++) {
|
||
char value = (term->vt.private >> (i * 8)) & 0xff;
|
||
if (value == 0)
|
||
break;
|
||
c += snprintf(&msg[c], sizeof(msg) - c, "%c", value);
|
||
}
|
||
|
||
xassert(term->vt.params.idx == 0);
|
||
|
||
snprintf(&msg[c], sizeof(msg) - c, "%c", final);
|
||
return msg;
|
||
|
||
}
|
||
#endif
|
||
|
||
static void
|
||
action_ignore(struct terminal *term)
|
||
{
|
||
}
|
||
|
||
static void
|
||
action_clear(struct terminal *term)
|
||
{
|
||
term->vt.params.idx = 0;
|
||
term->vt.private = 0;
|
||
}
|
||
|
||
static void
|
||
action_execute(struct terminal *term, uint8_t c)
|
||
{
|
||
LOG_DBG("execute: 0x%02x", c);
|
||
switch (c) {
|
||
|
||
/*
|
||
* 7-bit C0 control characters
|
||
*/
|
||
|
||
case '\0':
|
||
break;
|
||
|
||
case '\a':
|
||
/* BEL - bell */
|
||
term_bell(term);
|
||
break;
|
||
|
||
case '\b':
|
||
/* backspace */
|
||
#if 0
|
||
/*
|
||
* This is the “correct” BS behavior. However, it doesn’t play
|
||
* nicely with bw/auto_left_margin, hence the alternative
|
||
* implementation below.
|
||
*
|
||
* Note that it breaks vttest “1. Test of cursor movements ->
|
||
* Test of autowrap”
|
||
*/
|
||
term_cursor_left(term, 1);
|
||
#else
|
||
if (term->grid->cursor.lcf)
|
||
term->grid->cursor.lcf = false;
|
||
else {
|
||
/* Reverse wrap */
|
||
if (unlikely(term->grid->cursor.point.col == 0) &&
|
||
likely(term->reverse_wrap && term->auto_margin))
|
||
{
|
||
if (term->grid->cursor.point.row <= term->scroll_region.start) {
|
||
/* Don’t wrap past, or inside, the scrolling region(?) */
|
||
} else
|
||
term_cursor_to(
|
||
term,
|
||
term->grid->cursor.point.row - 1,
|
||
term->cols - 1);
|
||
} else
|
||
term_cursor_left(term, 1);
|
||
}
|
||
#endif
|
||
break;
|
||
|
||
case '\t': {
|
||
/* HT - horizontal tab */
|
||
int start_col = term->grid->cursor.point.col;
|
||
int new_col = term->cols - 1;
|
||
|
||
tll_foreach(term->tab_stops, it) {
|
||
if (it->item > start_col) {
|
||
new_col = it->item;
|
||
break;
|
||
}
|
||
}
|
||
xassert(new_col >= start_col);
|
||
xassert(new_col < term->cols);
|
||
|
||
struct row *row = term->grid->cur_row;
|
||
|
||
bool emit_tab_char = (row->cells[start_col].wc == 0 ||
|
||
row->cells[start_col].wc == L' ');
|
||
|
||
/* Check if all cells from here until the next tab stop are empty */
|
||
for (const struct cell *cell = &row->cells[start_col + 1];
|
||
cell < &row->cells[new_col];
|
||
cell++)
|
||
{
|
||
if (!(cell->wc == 0 || cell->wc == L' ')) {
|
||
emit_tab_char = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Emit a tab in current cell, and write spaces to the
|
||
* subsequent cells, all the way until the next tab stop.
|
||
*/
|
||
if (emit_tab_char) {
|
||
row->dirty = true;
|
||
|
||
row->cells[start_col].wc = '\t';
|
||
row->cells[start_col].attrs.clean = 0;
|
||
|
||
for (struct cell *cell = &row->cells[start_col + 1];
|
||
cell < &row->cells[new_col];
|
||
cell++)
|
||
{
|
||
cell->wc = L' ';
|
||
cell->attrs.clean = 0;
|
||
}
|
||
}
|
||
|
||
/* According to the specification, HT _should_ cancel LCF. But
|
||
* XTerm, and nearly all other emulators, don't. So we follow
|
||
* suit */
|
||
bool lcf = term->grid->cursor.lcf;
|
||
term_cursor_right(term, new_col - start_col);
|
||
term->grid->cursor.lcf = lcf;
|
||
break;
|
||
}
|
||
|
||
case '\n':
|
||
case '\v':
|
||
case '\f':
|
||
/* LF - \n - line feed */
|
||
/* VT - \v - vertical tab */
|
||
/* FF - \f - form feed */
|
||
term_linefeed(term);
|
||
break;
|
||
|
||
case '\r':
|
||
/* CR - carriage ret */
|
||
term_carriage_return(term);
|
||
break;
|
||
|
||
case '\x0e':
|
||
/* SO - shift out */
|
||
term->charsets.selected = G1;
|
||
term_update_ascii_printer(term);
|
||
break;
|
||
|
||
case '\x0f':
|
||
/* SI - shift in */
|
||
term->charsets.selected = G0;
|
||
term_update_ascii_printer(term);
|
||
break;
|
||
|
||
/*
|
||
* 8-bit C1 control characters
|
||
*
|
||
* We ignore these, but keep them here for reference, along
|
||
* with their corresponding 7-bit variants.
|
||
*
|
||
* As far as I can tell, XTerm also ignores these _when in
|
||
* UTF-8 mode_. Which would be the normal mode of operation
|
||
* these days. And since we _only_ support UTF-8...
|
||
*/
|
||
|
||
#if 0
|
||
case '\x84': /* IND -> ESC D */
|
||
case '\x85': /* NEL -> ESC E */
|
||
case '\x88': /* Tab Set -> ESC H */
|
||
case '\x8d': /* RI -> ESC M */
|
||
case '\x8e': /* SS2 -> ESC N */
|
||
case '\x8f': /* SS3 -> ESC O */
|
||
case '\x90': /* DCS -> ESC P */
|
||
case '\x96': /* SPA -> ESC V */
|
||
case '\x97': /* EPA -> ESC W */
|
||
case '\x98': /* SOS -> ESC X */
|
||
case '\x9a': /* DECID -> ESC Z (obsolete form of CSI c) */
|
||
case '\x9b': /* CSI -> ESC [ */
|
||
case '\x9c': /* ST -> ESC \ */
|
||
case '\x9d': /* OSC -> ESC ] */
|
||
case '\x9e': /* PM -> ESC ^ */
|
||
case '\x9f': /* APC -> ESC _ */
|
||
break;
|
||
#endif
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
static void
|
||
action_print(struct terminal *term, uint8_t c)
|
||
{
|
||
term_reset_grapheme_state(term);
|
||
term->ascii_printer(term, c);
|
||
}
|
||
|
||
static void
|
||
action_param(struct terminal *term, uint8_t c)
|
||
{
|
||
if (term->vt.params.idx == 0) {
|
||
struct vt_param *param = &term->vt.params.v[0];
|
||
param->value = 0;
|
||
param->sub.idx = 0;
|
||
term->vt.params.idx = 1;
|
||
}
|
||
|
||
xassert(term->vt.params.idx > 0);
|
||
|
||
const size_t max_params
|
||
= sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0]);
|
||
const size_t max_sub_params
|
||
= sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0]);
|
||
|
||
/* New parameter */
|
||
if (c == ';') {
|
||
if (unlikely(term->vt.params.idx >= max_params))
|
||
goto excess_params;
|
||
|
||
struct vt_param *param = &term->vt.params.v[term->vt.params.idx++];
|
||
param->value = 0;
|
||
param->sub.idx = 0;
|
||
}
|
||
|
||
/* New sub-parameter */
|
||
else if (c == ':') {
|
||
if (unlikely(term->vt.params.idx - 1 >= max_params))
|
||
goto excess_params;
|
||
|
||
struct vt_param *param = &term->vt.params.v[term->vt.params.idx - 1];
|
||
if (unlikely(param->sub.idx >= max_sub_params))
|
||
goto excess_sub_params;
|
||
|
||
param->sub.value[param->sub.idx++] = 0;
|
||
}
|
||
|
||
/* New digit for current parameter/sub-parameter */
|
||
else {
|
||
if (unlikely(term->vt.params.idx - 1 >= max_params))
|
||
goto excess_params;
|
||
|
||
struct vt_param *param = &term->vt.params.v[term->vt.params.idx - 1];
|
||
unsigned *value;
|
||
|
||
if (param->sub.idx > 0) {
|
||
if (unlikely(param->sub.idx - 1 >= max_sub_params))
|
||
goto excess_sub_params;
|
||
value = ¶m->sub.value[param->sub.idx - 1];
|
||
} else
|
||
value = ¶m->value;
|
||
|
||
*value *= 10;
|
||
*value += c - '0';
|
||
}
|
||
|
||
#if defined(_DEBUG)
|
||
/* The rest of the code assumes 'idx' *never* points outside the array */
|
||
xassert(term->vt.params.idx <= max_params);
|
||
for (size_t i = 0; i < term->vt.params.idx; i++)
|
||
xassert(term->vt.params.v[i].sub.idx <= max_sub_params);
|
||
#endif
|
||
|
||
return;
|
||
|
||
excess_params:
|
||
{
|
||
static bool have_warned = false;
|
||
if (!have_warned) {
|
||
have_warned = true;
|
||
LOG_WARN(
|
||
"unsupported: escape with more than %zu parameters "
|
||
"(will not warn again)",
|
||
sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0]));
|
||
}
|
||
}
|
||
return;
|
||
|
||
excess_sub_params:
|
||
{
|
||
static bool have_warned = false;
|
||
if (!have_warned) {
|
||
have_warned = true;
|
||
LOG_WARN(
|
||
"unsupported: escape with more than %zu sub-parameters "
|
||
"(will not warn again)",
|
||
sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0]));
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
static void
|
||
action_collect(struct terminal *term, uint8_t c)
|
||
{
|
||
LOG_DBG("collect: %c", c);
|
||
|
||
/*
|
||
* Having more than one private is *very* rare. Foot only supports
|
||
* a *single* escape with two privates, and none with three or
|
||
* more.
|
||
*
|
||
* As such, we optimize *reading* the private(s), and *resetting*
|
||
* them (in action_clear()). Writing is ok if it’s a bit slow.
|
||
*/
|
||
|
||
if ((term->vt.private & 0xff) == 0)
|
||
term->vt.private = c;
|
||
else if (((term->vt.private >> 8) & 0xff) == 0)
|
||
term->vt.private |= c << 8;
|
||
else if (((term->vt.private >> 16) & 0xff) == 0)
|
||
term->vt.private |= c << 16;
|
||
else if (((term->vt.private >> 24) & 0xff) == 0)
|
||
term->vt.private |= c << 24;
|
||
else
|
||
LOG_WARN("only four private/intermediate characters supported");
|
||
}
|
||
|
||
static void
|
||
action_esc_dispatch(struct terminal *term, uint8_t final)
|
||
{
|
||
LOG_DBG("ESC: %s", esc_as_string(term, final));
|
||
|
||
switch (term->vt.private) {
|
||
case 0:
|
||
switch (final) {
|
||
case '7':
|
||
term_save_cursor(term);
|
||
break;
|
||
|
||
case '8':
|
||
term_restore_cursor(term, &term->grid->saved_cursor);
|
||
break;
|
||
|
||
case 'c':
|
||
term_reset(term, true);
|
||
break;
|
||
|
||
case 'n':
|
||
/* LS2 - Locking Shift 2 */
|
||
term->charsets.selected = G2;
|
||
term_update_ascii_printer(term);
|
||
break;
|
||
|
||
case 'o':
|
||
/* LS3 - Locking Shift 3 */
|
||
term->charsets.selected = G3;
|
||
term_update_ascii_printer(term);
|
||
break;
|
||
|
||
case 'D':
|
||
term_linefeed(term);
|
||
break;
|
||
|
||
case 'E':
|
||
term_carriage_return(term);
|
||
term_linefeed(term);
|
||
break;
|
||
|
||
case 'H':
|
||
tll_foreach(term->tab_stops, it) {
|
||
if (it->item >= term->grid->cursor.point.col) {
|
||
tll_insert_before(term->tab_stops, it, term->grid->cursor.point.col);
|
||
break;
|
||
}
|
||
}
|
||
|
||
tll_push_back(term->tab_stops, term->grid->cursor.point.col);
|
||
break;
|
||
|
||
case 'M':
|
||
term_reverse_index(term);
|
||
break;
|
||
|
||
case 'N':
|
||
/* SS2 - Single Shift 2 */
|
||
term_single_shift(term, G2);
|
||
break;
|
||
|
||
case 'O':
|
||
/* SS3 - Single Shift 3 */
|
||
term_single_shift(term, G3);
|
||
break;
|
||
|
||
case '\\':
|
||
/* ST - String Terminator */
|
||
break;
|
||
|
||
case '=':
|
||
term->keypad_keys_mode = KEYPAD_APPLICATION;
|
||
break;
|
||
|
||
case '>':
|
||
term->keypad_keys_mode = KEYPAD_NUMERICAL;
|
||
break;
|
||
|
||
default:
|
||
UNHANDLED();
|
||
break;
|
||
}
|
||
break; /* private[0] == 0 */
|
||
|
||
// Designate character set
|
||
case '(': // G0
|
||
case ')': // G1
|
||
case '*': // G2
|
||
case '+': // G3
|
||
switch (final) {
|
||
case '0': {
|
||
size_t idx = term->vt.private - '(';
|
||
xassert(idx <= G3);
|
||
term->charsets.set[idx] = CHARSET_GRAPHIC;
|
||
term_update_ascii_printer(term);
|
||
break;
|
||
}
|
||
|
||
case 'B': {
|
||
size_t idx = term->vt.private - '(';
|
||
xassert(idx <= G3);
|
||
term->charsets.set[idx] = CHARSET_ASCII;
|
||
term_update_ascii_printer(term);
|
||
break;
|
||
}
|
||
}
|
||
break;
|
||
|
||
case '#':
|
||
switch (final) {
|
||
case '8':
|
||
for (int r = 0; r < term->rows; r++) {
|
||
struct row *row = grid_row(term->grid, r);
|
||
for (int c = 0; c < term->cols; c++) {
|
||
row->cells[c].wc = L'E';
|
||
row->cells[c].attrs = (struct attributes){0};
|
||
}
|
||
row->dirty = true;
|
||
}
|
||
break;
|
||
}
|
||
break; /* private[0] == '#' */
|
||
|
||
}
|
||
}
|
||
|
||
static void
|
||
action_csi_dispatch(struct terminal *term, uint8_t c)
|
||
{
|
||
csi_dispatch(term, c);
|
||
}
|
||
|
||
static void
|
||
action_osc_start(struct terminal *term, uint8_t c)
|
||
{
|
||
term->vt.osc.idx = 0;
|
||
}
|
||
|
||
static void
|
||
action_osc_end(struct terminal *term, uint8_t c)
|
||
{
|
||
if (!osc_ensure_size(term, term->vt.osc.idx + 1))
|
||
return;
|
||
term->vt.osc.data[term->vt.osc.idx] = '\0';
|
||
osc_dispatch(term);
|
||
}
|
||
|
||
static void
|
||
action_osc_put(struct terminal *term, uint8_t c)
|
||
{
|
||
if (!osc_ensure_size(term, term->vt.osc.idx + 1))
|
||
return;
|
||
term->vt.osc.data[term->vt.osc.idx++] = c;
|
||
}
|
||
|
||
static void
|
||
action_hook(struct terminal *term, uint8_t c)
|
||
{
|
||
dcs_hook(term, c);
|
||
}
|
||
|
||
static void
|
||
action_unhook(struct terminal *term, uint8_t c)
|
||
{
|
||
dcs_unhook(term);
|
||
}
|
||
|
||
static void
|
||
action_put(struct terminal *term, uint8_t c)
|
||
{
|
||
dcs_put(term, c);
|
||
}
|
||
|
||
static inline uint32_t
|
||
chain_key(uint32_t old_key, uint32_t new_wc)
|
||
{
|
||
/* Rotate left 8 bits, xor with new char */
|
||
return ((old_key << 8) | (old_key >> (32 - 8))) ^ new_wc;
|
||
}
|
||
|
||
static void
|
||
action_utf8_print(struct terminal *term, wchar_t wc)
|
||
{
|
||
int width = wcwidth(wc);
|
||
const bool grapheme_clustering = term->conf->tweak.grapheme_shaping;
|
||
|
||
#if !defined(FOOT_GRAPHEME_CLUSTERING)
|
||
xassert(!grapheme_clustering);
|
||
#endif
|
||
|
||
if (term->grid->cursor.point.col > 0 &&
|
||
(grapheme_clustering ||
|
||
(!grapheme_clustering && width == 0 && wc >= 0x300)))
|
||
{
|
||
int col = term->grid->cursor.point.col;
|
||
if (!term->grid->cursor.lcf)
|
||
col--;
|
||
|
||
/* Skip past spacers */
|
||
struct row *row = term->grid->cur_row;
|
||
while (row->cells[col].wc >= CELL_SPACER && col > 0)
|
||
col--;
|
||
|
||
xassert(col >= 0 && col < term->cols);
|
||
wchar_t base = row->cells[col].wc;
|
||
wchar_t UNUSED last = base;
|
||
|
||
/* Is base cell already a cluster? */
|
||
const struct composed *composed =
|
||
(base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI)
|
||
? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO)
|
||
: NULL;
|
||
|
||
uint32_t key;
|
||
|
||
if (composed != NULL) {
|
||
base = composed->chars[0];
|
||
last = composed->chars[composed->count - 1];
|
||
key = chain_key(composed->key, wc);
|
||
} else
|
||
key = chain_key(base, wc);
|
||
|
||
key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO;
|
||
|
||
#if defined(FOOT_GRAPHEME_CLUSTERING)
|
||
if (grapheme_clustering) {
|
||
/* Check if we're on a grapheme cluster break */
|
||
if (utf8proc_grapheme_break_stateful(
|
||
last, wc, &term->vt.grapheme_state))
|
||
{
|
||
term_reset_grapheme_state(term);
|
||
goto out;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
int base_width = wcwidth(base);
|
||
if (base_width > 0) {
|
||
term->grid->cursor.point.col = col;
|
||
term->grid->cursor.lcf = false;
|
||
|
||
if (composed == NULL) {
|
||
bool base_from_primary;
|
||
bool comb_from_primary;
|
||
bool pre_from_primary;
|
||
|
||
wchar_t precomposed = fcft_precompose(
|
||
term->fonts[0], base, wc, &base_from_primary,
|
||
&comb_from_primary, &pre_from_primary);
|
||
|
||
int precomposed_width = wcwidth(precomposed);
|
||
|
||
/*
|
||
* Only use the pre-composed character if:
|
||
*
|
||
* 1. we *have* a pre-composed character
|
||
* 2. the width matches the base characters width
|
||
* 3. it's in the primary font, OR one of the base or
|
||
* combining characters are *not* from the primary
|
||
* font
|
||
*/
|
||
|
||
if (precomposed != (wchar_t)-1 &&
|
||
precomposed_width == base_width &&
|
||
(pre_from_primary ||
|
||
!base_from_primary ||
|
||
!comb_from_primary))
|
||
{
|
||
wc = precomposed;
|
||
width = precomposed_width;
|
||
term_reset_grapheme_state(term);
|
||
goto out;
|
||
}
|
||
}
|
||
|
||
size_t wanted_count = composed != NULL ? composed->count + 1 : 2;
|
||
if (wanted_count > 255) {
|
||
xassert(composed != NULL);
|
||
|
||
#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
|
||
LOG_WARN("combining character overflow:");
|
||
LOG_WARN(" base: 0x%04x", composed->chars[0]);
|
||
for (size_t i = 1; i < composed->count; i++)
|
||
LOG_WARN(" cc: 0x%04x", composed->chars[i]);
|
||
LOG_ERR(" new: 0x%04x", wc);
|
||
#endif
|
||
/* This is going to break anyway... */
|
||
wanted_count--;
|
||
}
|
||
|
||
xassert(wanted_count <= 255);
|
||
|
||
/* Look for existing combining chain */
|
||
while (true) {
|
||
const struct composed *cc = composed_lookup(term->composed, key);
|
||
if (cc == NULL)
|
||
break;
|
||
|
||
/*
|
||
* We may have a key collisison, so need to check that
|
||
* it’s a true match. If not, bump the key and try
|
||
* again.
|
||
*/
|
||
|
||
xassert(key == cc->key);
|
||
if (cc->chars[0] != base ||
|
||
cc->count != wanted_count ||
|
||
cc->chars[wanted_count - 1] != wc)
|
||
{
|
||
key++;
|
||
continue;
|
||
}
|
||
|
||
bool match = composed != NULL
|
||
? memcmp(&cc->chars[1], &composed->chars[1],
|
||
(wanted_count - 2) * sizeof(cc->chars[0])) == 0
|
||
: true;
|
||
|
||
if (!match) {
|
||
key++;
|
||
continue;
|
||
}
|
||
|
||
wc = CELL_COMB_CHARS_LO + cc->key;
|
||
width = cc->width;
|
||
goto out;
|
||
}
|
||
|
||
if (unlikely(term->composed_count >=
|
||
(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO)))
|
||
{
|
||
/* We reached our maximum number of allowed composed
|
||
* character chains. Fall through here and print the
|
||
* current zero-width character to the current cell */
|
||
LOG_WARN("maximum number of composed characters reached");
|
||
term_reset_grapheme_state(term);
|
||
goto out;
|
||
}
|
||
|
||
/* Allocate new chain */
|
||
struct composed *new_cc = xmalloc(sizeof(*new_cc));
|
||
new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0]));
|
||
new_cc->key = key;
|
||
new_cc->count = wanted_count;
|
||
new_cc->chars[0] = base;
|
||
new_cc->chars[wanted_count - 1] = wc;
|
||
|
||
if (composed != NULL) {
|
||
memcpy(&new_cc->chars[1], &composed->chars[1],
|
||
(wanted_count - 2) * sizeof(new_cc->chars[0]));
|
||
}
|
||
|
||
int grapheme_width = composed != NULL ? composed->width : base_width;
|
||
|
||
if (wc == 0xfe0f)
|
||
width = 2;
|
||
new_cc->width = min(max(grapheme_width, width), 2);
|
||
|
||
term->composed_count++;
|
||
composed_insert(&term->composed, new_cc);
|
||
|
||
wc = CELL_COMB_CHARS_LO + key;
|
||
width = grapheme_width;
|
||
|
||
xassert(wc >= CELL_COMB_CHARS_LO);
|
||
xassert(wc <= CELL_COMB_CHARS_HI);
|
||
goto out;
|
||
}
|
||
} else
|
||
term_reset_grapheme_state(term);
|
||
|
||
|
||
out:
|
||
if (width > 0)
|
||
term_print(term, wc, width);
|
||
}
|
||
|
||
static void
|
||
action_utf8_21(struct terminal *term, uint8_t c)
|
||
{
|
||
// wc = ((utf8[0] & 0x1f) << 6) | (utf8[1] & 0x3f)
|
||
term->vt.utf8 = (c & 0x1f) << 6;
|
||
}
|
||
|
||
static void
|
||
action_utf8_22(struct terminal *term, uint8_t c)
|
||
{
|
||
// wc = ((utf8[0] & 0x1f) << 6) | (utf8[1] & 0x3f)
|
||
term->vt.utf8 |= c & 0x3f;
|
||
action_utf8_print(term, term->vt.utf8);
|
||
}
|
||
|
||
static void
|
||
action_utf8_31(struct terminal *term, uint8_t c)
|
||
{
|
||
// wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f)
|
||
term->vt.utf8 = (c & 0x0f) << 12;
|
||
}
|
||
|
||
static void
|
||
action_utf8_32(struct terminal *term, uint8_t c)
|
||
{
|
||
// wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f)
|
||
term->vt.utf8 |= (c & 0x3f) << 6;
|
||
}
|
||
|
||
static void
|
||
action_utf8_33(struct terminal *term, uint8_t c)
|
||
{
|
||
// wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f)
|
||
term->vt.utf8 |= c & 0x3f;
|
||
action_utf8_print(term, term->vt.utf8);
|
||
}
|
||
|
||
static void
|
||
action_utf8_41(struct terminal *term, uint8_t c)
|
||
{
|
||
// wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f);
|
||
term->vt.utf8 = (c & 0x07) << 18;
|
||
}
|
||
|
||
static void
|
||
action_utf8_42(struct terminal *term, uint8_t c)
|
||
{
|
||
// wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f);
|
||
term->vt.utf8 |= (c & 0x3f) << 12;
|
||
}
|
||
|
||
static void
|
||
action_utf8_43(struct terminal *term, uint8_t c)
|
||
{
|
||
// wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f);
|
||
term->vt.utf8 |= (c & 0x3f) << 6;
|
||
}
|
||
|
||
static void
|
||
action_utf8_44(struct terminal *term, uint8_t c)
|
||
{
|
||
// wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f);
|
||
term->vt.utf8 |= c & 0x3f;
|
||
action_utf8_print(term, term->vt.utf8);
|
||
}
|
||
|
||
IGNORE_WARNING("-Wpedantic")
|
||
|
||
static enum state
|
||
anywhere(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x18: action_execute(term, data); return STATE_GROUND;
|
||
case 0x1a: action_execute(term, data); return STATE_GROUND;
|
||
case 0x1b: action_clear(term); return STATE_ESCAPE;
|
||
|
||
/* 8-bit C1 control characters (not supported) */
|
||
case 0x80 ... 0x9f: return STATE_GROUND;
|
||
}
|
||
|
||
return term->vt.state;
|
||
}
|
||
|
||
static enum state
|
||
state_ground_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_execute(term, data); return STATE_GROUND;
|
||
|
||
/* modified from 0x20..0x7f to 0x20..0x7e, since 0x7f is DEL, which is a zero-width character */
|
||
case 0x20 ... 0x7e: action_print(term, data); return STATE_GROUND;
|
||
|
||
case 0xc2 ... 0xdf: action_utf8_21(term, data); return STATE_UTF8_21;
|
||
case 0xe0 ... 0xef: action_utf8_31(term, data); return STATE_UTF8_31;
|
||
case 0xf0 ... 0xf4: action_utf8_41(term, data); return STATE_UTF8_41;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_escape_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_execute(term, data); return STATE_ESCAPE;
|
||
|
||
case 0x20 ... 0x2f: action_collect(term, data); return STATE_ESCAPE_INTERMEDIATE;
|
||
case 0x30 ... 0x4f: action_esc_dispatch(term, data); return STATE_GROUND;
|
||
case 0x50: action_clear(term); return STATE_DCS_ENTRY;
|
||
case 0x51 ... 0x57: action_esc_dispatch(term, data); return STATE_GROUND;
|
||
case 0x58: return STATE_SOS_PM_APC_STRING;
|
||
case 0x59: action_esc_dispatch(term, data); return STATE_GROUND;
|
||
case 0x5a: action_esc_dispatch(term, data); return STATE_GROUND;
|
||
case 0x5b: action_clear(term); return STATE_CSI_ENTRY;
|
||
case 0x5c: action_esc_dispatch(term, data); return STATE_GROUND;
|
||
case 0x5d: action_osc_start(term, data); return STATE_OSC_STRING;
|
||
case 0x5e ... 0x5f: return STATE_SOS_PM_APC_STRING;
|
||
case 0x60 ... 0x7e: action_esc_dispatch(term, data); return STATE_GROUND;
|
||
case 0x7f: action_ignore(term); return STATE_ESCAPE;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_escape_intermediate_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_execute(term, data); return STATE_ESCAPE_INTERMEDIATE;
|
||
|
||
case 0x20 ... 0x2f: action_collect(term, data); return STATE_ESCAPE_INTERMEDIATE;
|
||
case 0x30 ... 0x7e: action_esc_dispatch(term, data); return STATE_GROUND;
|
||
case 0x7f: action_ignore(term); return STATE_ESCAPE_INTERMEDIATE;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_csi_entry_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_ENTRY;
|
||
|
||
case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE;
|
||
case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM;
|
||
case 0x3a ... 0x3b: action_param(term, data); return STATE_CSI_PARAM;
|
||
case 0x3c ... 0x3f: action_collect(term, data); return STATE_CSI_PARAM;
|
||
case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND;
|
||
case 0x7f: action_ignore(term); return STATE_CSI_ENTRY;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_csi_param_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_PARAM;
|
||
|
||
case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE;
|
||
|
||
case 0x30 ... 0x39:
|
||
case 0x3a ... 0x3b: action_param(term, data); return STATE_CSI_PARAM;
|
||
|
||
case 0x3c ... 0x3f: return STATE_CSI_IGNORE;
|
||
case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND;
|
||
case 0x7f: action_ignore(term); return STATE_CSI_PARAM;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_csi_intermediate_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_INTERMEDIATE;
|
||
|
||
case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE;
|
||
case 0x30 ... 0x3f: return STATE_CSI_IGNORE;
|
||
case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND;
|
||
case 0x7f: action_ignore(term); return STATE_CSI_INTERMEDIATE;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_csi_ignore_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_IGNORE;
|
||
|
||
case 0x20 ... 0x3f: action_ignore(term); return STATE_CSI_IGNORE;
|
||
case 0x40 ... 0x7e: return STATE_GROUND;
|
||
case 0x7f: action_ignore(term); return STATE_CSI_IGNORE;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_osc_string_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
|
||
/* Note: original was 20-7f, but I changed to 20-ff to include utf-8. Don't forget to add EXECUTE to 8-bit C1 if we implement that. */
|
||
default: action_osc_put(term, data); return STATE_OSC_STRING;
|
||
|
||
case 0x07: action_osc_end(term, data); return STATE_GROUND;
|
||
|
||
case 0x00 ... 0x06:
|
||
case 0x08 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_ignore(term); return STATE_OSC_STRING;
|
||
|
||
|
||
case 0x18:
|
||
case 0x1a: action_osc_end(term, data); action_execute(term, data); return STATE_GROUND;
|
||
|
||
case 0x1b: action_osc_end(term, data); action_clear(term); return STATE_ESCAPE;
|
||
}
|
||
}
|
||
|
||
static enum state
|
||
state_dcs_entry_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_ENTRY;
|
||
|
||
case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE;
|
||
case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM;
|
||
case 0x3a: return STATE_DCS_IGNORE;
|
||
case 0x3b: action_param(term, data); return STATE_DCS_PARAM;
|
||
case 0x3c ... 0x3f: action_collect(term, data); return STATE_DCS_PARAM;
|
||
case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH;
|
||
case 0x7f: action_ignore(term); return STATE_DCS_ENTRY;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_dcs_param_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_PARAM;
|
||
|
||
case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE;
|
||
case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM;
|
||
case 0x3a: return STATE_DCS_IGNORE;
|
||
case 0x3b: action_param(term, data); return STATE_DCS_PARAM;
|
||
case 0x3c ... 0x3f: return STATE_DCS_IGNORE;
|
||
case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH;
|
||
case 0x7f: action_ignore(term); return STATE_DCS_PARAM;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_dcs_intermediate_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_INTERMEDIATE;
|
||
|
||
case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE;
|
||
case 0x30 ... 0x3f: return STATE_DCS_IGNORE;
|
||
case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH;
|
||
case 0x7f: action_ignore(term); return STATE_DCS_INTERMEDIATE;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_dcs_ignore_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x1f:
|
||
case 0x20 ... 0x7f: action_ignore(term); return STATE_DCS_IGNORE;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_dcs_passthrough_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x7e: action_put(term, data); return STATE_DCS_PASSTHROUGH;
|
||
|
||
case 0x7f: action_ignore(term); return STATE_DCS_PASSTHROUGH;
|
||
|
||
/* Anywhere */
|
||
case 0x18: action_unhook(term, data); action_execute(term, data); return STATE_GROUND;
|
||
case 0x1a: action_unhook(term, data); action_execute(term, data); return STATE_GROUND;
|
||
case 0x1b: action_unhook(term, data); action_clear(term); return STATE_ESCAPE;
|
||
|
||
/* 8-bit C1 control characters (not supported) */
|
||
case 0x80 ... 0x9f: action_unhook(term, data); return STATE_GROUND;
|
||
|
||
default: return STATE_DCS_PASSTHROUGH;
|
||
}
|
||
}
|
||
|
||
static enum state
|
||
state_sos_pm_apc_string_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x00 ... 0x17:
|
||
case 0x19:
|
||
case 0x1c ... 0x7f: action_ignore(term); return STATE_SOS_PM_APC_STRING;
|
||
}
|
||
|
||
return anywhere(term, data);
|
||
}
|
||
|
||
static enum state
|
||
state_utf8_21_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x80 ... 0xbf: action_utf8_22(term, data); return STATE_GROUND;
|
||
default: return STATE_GROUND;
|
||
}
|
||
}
|
||
|
||
static enum state
|
||
state_utf8_31_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x80 ... 0xbf: action_utf8_32(term, data); return STATE_UTF8_32;
|
||
default: return STATE_GROUND;
|
||
}
|
||
}
|
||
|
||
static enum state
|
||
state_utf8_32_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x80 ... 0xbf: action_utf8_33(term, data); return STATE_GROUND;
|
||
default: return STATE_GROUND;
|
||
}
|
||
}
|
||
|
||
static enum state
|
||
state_utf8_41_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x80 ... 0xbf: action_utf8_42(term, data); return STATE_UTF8_42;
|
||
default: return STATE_GROUND;
|
||
}
|
||
}
|
||
|
||
static enum state
|
||
state_utf8_42_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x80 ... 0xbf: action_utf8_43(term, data); return STATE_UTF8_43;
|
||
default: return STATE_GROUND;
|
||
}
|
||
}
|
||
|
||
static enum state
|
||
state_utf8_43_switch(struct terminal *term, uint8_t data)
|
||
{
|
||
switch (data) {
|
||
/* exit current enter new state */
|
||
case 0x80 ... 0xbf: action_utf8_44(term, data); return STATE_GROUND;
|
||
default: return STATE_GROUND;
|
||
}
|
||
}
|
||
|
||
UNIGNORE_WARNINGS
|
||
|
||
void
|
||
vt_from_slave(struct terminal *term, const uint8_t *data, size_t len)
|
||
{
|
||
enum state current_state = term->vt.state;
|
||
|
||
const uint8_t *p = data;
|
||
for (size_t i = 0; i < len; i++, p++) {
|
||
switch (current_state) {
|
||
case STATE_GROUND: current_state = state_ground_switch(term, *p); break;
|
||
case STATE_ESCAPE: current_state = state_escape_switch(term, *p); break;
|
||
case STATE_ESCAPE_INTERMEDIATE: current_state = state_escape_intermediate_switch(term, *p); break;
|
||
case STATE_CSI_ENTRY: current_state = state_csi_entry_switch(term, *p); break;
|
||
case STATE_CSI_PARAM: current_state = state_csi_param_switch(term, *p); break;
|
||
case STATE_CSI_INTERMEDIATE: current_state = state_csi_intermediate_switch(term, *p); break;
|
||
case STATE_CSI_IGNORE: current_state = state_csi_ignore_switch(term, *p); break;
|
||
case STATE_OSC_STRING: current_state = state_osc_string_switch(term, *p); break;
|
||
case STATE_DCS_ENTRY: current_state = state_dcs_entry_switch(term, *p); break;
|
||
case STATE_DCS_PARAM: current_state = state_dcs_param_switch(term, *p); break;
|
||
case STATE_DCS_INTERMEDIATE: current_state = state_dcs_intermediate_switch(term, *p); break;
|
||
case STATE_DCS_IGNORE: current_state = state_dcs_ignore_switch(term, *p); break;
|
||
case STATE_DCS_PASSTHROUGH: current_state = state_dcs_passthrough_switch(term, *p); break;
|
||
case STATE_SOS_PM_APC_STRING: current_state = state_sos_pm_apc_string_switch(term, *p); break;
|
||
|
||
case STATE_UTF8_21: current_state = state_utf8_21_switch(term, *p); break;
|
||
case STATE_UTF8_31: current_state = state_utf8_31_switch(term, *p); break;
|
||
case STATE_UTF8_32: current_state = state_utf8_32_switch(term, *p); break;
|
||
case STATE_UTF8_41: current_state = state_utf8_41_switch(term, *p); break;
|
||
case STATE_UTF8_42: current_state = state_utf8_42_switch(term, *p); break;
|
||
case STATE_UTF8_43: current_state = state_utf8_43_switch(term, *p); break;
|
||
}
|
||
|
||
term->vt.state = current_state;
|
||
}
|
||
}
|