commit - 17d56da6b3808a59155d2afbc938cd8484cc43b7
commit + 41aa0eed73347fe812858fd61ac90ba378a02500
blob - a2d0f85c129b3981c63a794afb366ca4adefbf63
blob + ba26133faea629c2563dd9fddba3ce0a0abd6c87
--- docs/design/m7-curses.md
+++ docs/design/m7-curses.md
paste placeholder, input sanitation (NUL rejected, ESC stripped,
64 KiB cap), and slash-completion cycling. Pure; `regress/editor`
drives key-op sequences and asserts buffer, cursor, and submission.
+- **ui_layout.c** — plain transcript rows as UTF-8 spans plus an optional
+ fill attribute and pane width. Pure; `regress/ui_layout` pins User prefixes,
+ continuation indentation, word and wide/combining-character wrapping,
+ narrow panes, drawer clipping, and resize reflow before curses paints them.
- **diff.c** — a unified-diff formatter, run in the tool executor role
(below); pure line diff to hunk text; `regress/diff`.
event (NUL rejected, other ESC stripped, 64 KiB cap, overflow
discarded with a notice). Decoded events feed the pure editor/UI
model; nothing about input depends on `wgetch`, so it is testable.
+The input row and the Owner's User transcript rows use one named palette
+treatment: high-contrast shaded rows on color terminals and bold reverse
+video otherwise. Every visual row of a User message is explicitly filled to
+the current transcript-pane edge, including its prefix and continuation
+indent; resize and drawer changes rebuild that edge from the pure layout.
`Ctrl-L` records a reflow-stable display boundary and forces a physical clear.
The tail view starts after that boundary, while scrolling up can still reach
earlier lines. It sends no coordinator message and does not alter conversation
blob - 4a797f13fee9166dbbba6ce13a27ab6b59a9a652
blob + 5c60dfb315d71a46f754cc8efd17bc2e4597688b
--- handoff/behavior.md
+++ handoff/behavior.md
bullet lists, inline code, bold/italic — via curses attributes.
Unmatched markers render literally. Ordinary assistant text is light
grey when color is available; markdown-specific colors take precedence.
- User text renders plain.
+ The Owner's User messages share a shaded, high-contrast treatment with
+ the input row. The `> ` prefix, wrapped or hard-break continuation rows,
+ and padding through the transcript pane's right edge all carry that
+ treatment; an open conversation drawer narrows the edge. Monochrome
+ terminals use bold reverse video. Line and print modes remain plain.
- Tool calls appear as they run, indented and visually distinct from prose.
Each `tool:`-prefixed line includes a bounded, control-safe allowlist of
useful arguments: for example, a read's path and line window or a grep's
blob - f53727c56c505a66fcf3934d6fc5b9b0af9c46c4
blob + 6279752052e03e05a5c783672834e84a75cbde05
--- handoff/verification.md
+++ handoff/verification.md
hostile diff bytes are length-checked at the role boundary (I13);
and a resume/projection check proves diff output appears in
neither the provider projection nor the replayed journal (I14).
+- **Owner-row treatment:** the pure UI layout suite pins the shared color and
+ monochrome attributes, User prefix and continuation geometry, explicit
+ full-pane fill, empty multiline rows, word wrapping, wide/combining UTF-8,
+ narrow-pane fallback, drawer clipping, and resize reflow. The curses pty
+ path exercises the styled input and transcript while retaining clean
+ terminal-mode startup and teardown.
- **Prompt caching:** request-builder assertions across consecutive
requests — breakpoints on the final tool definition, the
block-form system prompt, and the advancing tail; cold after
blob - c3f2ea6f4cc01df501a740da95851e17c54bc553
blob + 402a81eb1c7aaf5958cf491f6a98afa86a3fcc1c
--- regress/Makefile
+++ regress/Makefile
SUBDIR= agentcfg anthropic buf claude_subscription compaction conf deploy diff editor generation http imsgev journal json local_port log markdown \
model_window openai output print sandbox skills sse term tool_display tools turn \
- turn_mechanics turn_txn ui_config web xmalloc
+ turn_mechanics turn_txn ui_config ui_layout web xmalloc
.include <bsd.subdir.mk>
blob - 3c5f8488d910e615a68f0ea2110ab7eac0b1f7bf
blob + ed15645241409d09070e027ca8035f9eaf16f64e
--- regress/README
+++ regress/README
codes, key flow, transactional cancel)
line mode (behavior.md 2.2) turn (multi-turn piped session,
slash commands, /model refused)
+User transcript/input treatment ui_layout (attributes, prefix and
+ continuation geometry, full-pane fill,
+ UTF-8 and reflow); turn (monochrome pty
+ startup and teardown)
slash commands (behavior.md 3) turn, print (/clear, /context, /compact,
/quit and Skill dispatch); journal
(clear replay)
src/fugu/turn_txn.c turn_txn (pure ordering plus real-Journal
integration); turn (cache/SIGINT path)
src/fugu-tty/ui_config_rx.c ui_config; turn (full-palette PTY path)
+src/fugu-tty/ui_layout.c,
+ ui_palette.c ui_layout; turn (curses PTY path)
src/common/imsgev.c imsgev (whole-payload views, checked
normal/teardown sends, bounded chunk
emission, event failures);
blob - 1d654133634f2852306ac68ef95b3652f76f93b3
blob + d04352ab3fb3080232ecdb2227fb07527239e0e9
--- regress/turn/run.sh
+++ regress/turn/run.sh
rm -rf "$HOME/.fugu"
start_stub "CONTEXT"
write_conf "$stubport"
- printf 'hello\r/quit\r' | TERM=vt100 HOME=$HOME \
+ printf 'OWNER-BLOCK-MARKER\r/quit\r' | TERM=vt100 HOME=$HOME \
FUGU_CONF=$dir/fugu.conf FUGU_LIBEXEC=$dir/libexec/fugu \
FUGU_CA_FILE=$dir/cert.pem FUGU_RETRY_BASE_MS=50 \
"$obj/ptydrive" "$fugu" >"$dir/cursesout" 2>&1
grep -a -Fq "${esc}[?1006l" "$dir/cursesout" &&
grep -a -Fq "${esc}[?1000l" "$dir/cursesout"
ok "curses: mouse reporting is enabled and restored" $?
- grep -q "hello" "$dir/reqout" 2>/dev/null
+ grep -q "OWNER-BLOCK-MARKER" "$dir/reqout" 2>/dev/null
ok "curses: the typed prompt reached the provider" $?
+ grep -a -Fq "OWNER-BLOCK-MARKER" "$dir/cursesout"
+ ok "curses: Owner marker renders over a monochrome pty" $?
grep -a -Fq "Context 46k/200k 23%" "$dir/cursesout"
ok "curses: status shows Lead cache plus session context" $?
n=$(grep -c '"outcome":"ok"' "$sessions"/*.ndjson 2>/dev/null || echo 0)
blob - /dev/null
blob + d5e67a6bd303a05123ca0616e4956ac8995b29a3 (mode 644)
--- /dev/null
+++ regress/ui_layout/Makefile
+PROG= ui_layout_test
+SRCS= ui_layout_test.c ui_layout.c ui_palette.c utf8.c buf.c log.c \
+ xmalloc.c
+NOMAN= yes
+
+CFLAGS+= -I${.CURDIR}/../../src/fugu-tty
+LDADD= -lcurses
+DPADD= ${LIBCURSES}
+.PATH: ${.CURDIR}/../../src/fugu-tty
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + 2ff3486fbd247746fb4e110747f2f0b72b2092d0 (mode 644)
--- /dev/null
+++ regress/ui_layout/ui_layout_test.c
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * 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.
+ */
+
+#include <curses.h>
+
+#include <string.h>
+
+#include "buf.h"
+#include "ui_layout.h"
+#include "ui_palette.h"
+#include "regress.h"
+
+static int
+line_is(const struct ui_line *line, const char *want)
+{
+ struct buf buf;
+ size_t i;
+ int ok;
+
+ buf_init(&buf);
+ for (i = 0; i < line->nspans; i++)
+ buf_add(&buf, line->spans[i].text, line->spans[i].len);
+ buf_addc(&buf, '\0');
+ ok = strcmp((const char *)buf.data, want) == 0;
+ buf_free(&buf);
+ return (ok);
+}
+
+static int
+line_attrs_are(const struct ui_line *line, int attr)
+{
+ size_t i;
+
+ for (i = 0; i < line->nspans; i++)
+ if (line->spans[i].attr != attr)
+ return (0);
+ return (1);
+}
+
+static int
+line_width(const struct ui_line *line)
+{
+ size_t i;
+ int width = 0;
+
+ for (i = 0; i < line->nspans; i++)
+ width += line->spans[i].width;
+ return (width);
+}
+
+int
+main(void)
+{
+ static const char wide_combining[] =
+ "A\xcc\x81\xe4\xb8\xad" "B";
+ struct ui_layout layout = { 0 };
+ int attr = 0x1234;
+
+ /* One named treatment serves color and monochrome surfaces. */
+ CHECK(ui_owner_attr(0) == (A_BOLD | A_REVERSE));
+ CHECK(ui_owner_attr(1) == (A_BOLD | COLOR_PAIR(P_OWNER)));
+
+ ui_layout_plain(&layout, "hello", 5, 10, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 1);
+ CHECK(line_is(&layout.lines[0], "> hello"));
+ CHECK(layout.lines[0].fill_width == 10);
+ CHECK(layout.lines[0].fill_attr == attr);
+ CHECK(line_attrs_are(&layout.lines[0], attr));
+ CHECK(ui_layout_visible_fill(&layout.lines[0], 10, 0) == 10);
+ CHECK(ui_layout_visible_fill(&layout.lines[0], 8, 3) == 5);
+ CHECK(ui_layout_visible_fill(&layout.lines[0], 3, 3) == 0);
+
+ /* Every wrapped row has the prefix indent and the same full-width fill. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "abcdefghi", 9, 6, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 3);
+ CHECK(line_is(&layout.lines[0], "> abcd"));
+ CHECK(line_is(&layout.lines[1], " efgh"));
+ CHECK(line_is(&layout.lines[2], " i"));
+ CHECK(layout.lines[0].fill_width == 6 &&
+ layout.lines[1].fill_width == 6 &&
+ layout.lines[2].fill_width == 6);
+ CHECK(line_attrs_are(&layout.lines[0], attr) &&
+ line_attrs_are(&layout.lines[1], attr) &&
+ line_attrs_are(&layout.lines[2], attr));
+
+ /* Hard blank lines remain part of one coherent User block. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "a\n\nb", 4, 8, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 3);
+ CHECK(line_is(&layout.lines[0], "> a"));
+ CHECK(line_is(&layout.lines[1], " "));
+ CHECK(line_is(&layout.lines[2], " b"));
+ CHECK(layout.lines[1].fill_width == 8 &&
+ layout.lines[1].fill_attr == attr);
+
+ /* Word wrapping trims the break and reports the retained cell width. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "one two", 7, 8, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 2);
+ CHECK(line_is(&layout.lines[0], "> one"));
+ CHECK(layout.lines[0].nspans == 2 &&
+ layout.lines[0].spans[1].width == 3);
+ CHECK(line_is(&layout.lines[1], " two"));
+
+ /* Combining and wide cells use display width, not byte length. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, wide_combining, sizeof(wide_combining) - 1,
+ 6, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 1);
+ CHECK(layout.lines[0].nspans == 2 &&
+ layout.lines[0].spans[1].len == sizeof(wide_combining) - 1 &&
+ layout.lines[0].spans[1].width == 4);
+ CHECK(layout.lines[0].fill_width == 6);
+
+ /* Re-rendering at a narrower pane pins resize and drawer reflow. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "abcdef", 6, 5, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 2 && layout.lines[0].fill_width == 5);
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "abcdef", 6, 4, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 3 && layout.lines[0].fill_width == 4 &&
+ layout.lines[2].fill_width == 4);
+
+ /* Prefixes yield to content at widths one and two; nothing overdraws. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "ab", 2, 1, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 3);
+ CHECK(line_is(&layout.lines[0], ">"));
+ CHECK(line_is(&layout.lines[1], "a"));
+ CHECK(line_is(&layout.lines[2], "b"));
+ CHECK(line_width(&layout.lines[0]) <= 1 &&
+ line_width(&layout.lines[1]) <= 1 &&
+ line_width(&layout.lines[2]) <= 1);
+
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "ab", 2, 2, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 3);
+ CHECK(line_is(&layout.lines[0], "> "));
+ CHECK(line_is(&layout.lines[1], " a"));
+ CHECK(line_is(&layout.lines[2], " b"));
+ CHECK(line_width(&layout.lines[0]) <= 2 &&
+ line_width(&layout.lines[1]) <= 2 &&
+ line_width(&layout.lines[2]) <= 2);
+
+ /* A glyph wider than the whole pane becomes one visible placeholder. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "\xe4\xb8\xad", 3, 1, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 2 && line_is(&layout.lines[0], ">") &&
+ line_is(&layout.lines[1], "?"));
+ CHECK(line_width(&layout.lines[0]) == 1 &&
+ line_width(&layout.lines[1]) == 1);
+
+ /* Other transcript kinds retain ordinary, unfilled lines. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "assistant", 9, 20, 7, NULL, NULL, 0);
+ CHECK(layout.nlines == 1 && layout.lines[0].fill_width == 0);
+ CHECK(ui_layout_visible_fill(&layout.lines[0], 80, 31) == 0);
+ CHECK(line_attrs_are(&layout.lines[0], 7));
+
+ /* An empty User message still owns its prefix and padded row. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, NULL, 0, 12, attr, "> ", " ", 1);
+ CHECK(layout.nlines == 1 && line_is(&layout.lines[0], "> "));
+ CHECK(layout.lines[0].fill_width == 12);
+
+ ui_layout_free(&layout);
+ CHECK(layout.lines == NULL && layout.nlines == 0 && layout.cap == 0);
+ REGRESS_END();
+}
blob - f7c35cbc03654903dfa84d3caded4baf0ebccbea
blob + ac80260ee463e3dcd4a63f24b029ef2cae430ac0
--- src/fugu/fugu.1
+++ src/fugu/fugu.1
The curses interface displays a scrolling transcript, a one-line modal
editor, and a status bar.
Assistant markdown headings, lists, fenced code, emphasis, and inline code
-are rendered with terminal attributes; user messages remain plain text.
+are rendered with terminal attributes.
+The Owner's User messages share a shaded, high-contrast treatment with the
+input row; every wrapped row fills the current transcript pane.
+Monochrome terminals use bold reverse video.
Tool calls, errors, and edit diffs are visually distinct.
The status bar identifies idle, active-turn, summarization, and retry states,
and shows
blob - c85c3f89ffa51c2863e5d736f1543051bb983324
blob + 31c9a5089db29c5e37b47f4841a1ed13e884411b
--- src/fugu-tty/Makefile
+++ src/fugu-tty/Makefile
PROG= fugu-tty
-SRCS= main.c ui.c ui_config_rx.c markdown.c editor.c term_input.c utf8.c \
- worker.c imsgev.c buf.c log.c xmalloc.c
+SRCS= main.c ui.c ui_config_rx.c ui_layout.c ui_palette.c markdown.c \
+ editor.c term_input.c utf8.c worker.c imsgev.c buf.c log.c xmalloc.c
BINDIR= ${LIBEXECDIR}/fugu
MAN=
blob - 9c20233d383357a221d7316c0cccb7b58dfad897
blob + b20c911fc66c960252025317d69aa62cca89493c
--- src/fugu-tty/ui.c
+++ src/fugu-tty/ui.c
#include "editor.h"
#include "term_input.h"
#include "ui_config_rx.h"
+#include "ui_layout.h"
+#include "ui_palette.h"
#include "ui.h"
/* transcript item kinds */
struct buf raw;
};
-/* a rendered display line: coalesced spans with resolved curses attrs */
-struct span {
- int attr;
- char *text;
- size_t len;
- int width;
-};
-struct dline {
- struct span *spans;
- size_t n;
-};
-
-/* curses color pairs (when the terminal has color) */
-#define P_HEAD 1
-#define P_CODE 2
-#define P_ADD 3
-#define P_DEL 4
-#define P_TOOL 5
-#define P_ERR 6
-#define P_DIM 7
-#define P_TEXT 8
-
/* the conversation drawer (behavior.md 2.1): reference width, and the
* narrowest terminal it will occupy without starving the transcript */
#define DRAWER_W 30
/* transcript and its rendered scrollback */
struct titem *items;
size_t nitems, icap;
- struct dline *lines;
- size_t nlines, lcap;
+ struct ui_layout layout;
int dirty;
int scroll; /* top line; -1 follows the tail */
int clear_line; /* latest Ctrl-L display boundary */
return (a);
}
-/* ---- rendered-line construction ------------------------------------- */
-
-static void
-lines_reset(struct ui *u)
-{
- size_t i, j;
-
- for (i = 0; i < u->nlines; i++) {
- for (j = 0; j < u->lines[i].n; j++)
- free(u->lines[i].spans[j].text);
- free(u->lines[i].spans);
- }
- u->nlines = 0;
-}
-
-static struct dline *
-line_new(struct ui *u)
-{
- if (u->nlines == u->lcap) {
- u->lcap = u->lcap ? u->lcap * 2 : 128;
- u->lines = xreallocarray(u->lines, u->lcap, sizeof(*u->lines));
- }
- memset(&u->lines[u->nlines], 0, sizeof(u->lines[u->nlines]));
- return (&u->lines[u->nlines++]);
-}
-
-static void
-line_add(struct dline *l, int attr, const char *text, size_t len, int width)
-{
- struct span *s;
-
- l->spans = xreallocarray(l->spans, l->n + 1, sizeof(*l->spans));
- s = &l->spans[l->n++];
- s->attr = attr;
- s->width = width;
- s->len = len;
- s->text = xmalloc(len + 1);
- memcpy(s->text, text, len);
- s->text[len] = '\0';
-}
-
-/* Wrap plain UTF-8 text to width columns, one attr, into new lines. */
-static void
-render_plain(struct ui *u, const char *text, size_t len, int width, int attr,
- const char *prefix, const char *continuation)
-{
- struct dline *l;
- size_t i = 0;
- int col;
- size_t last_break, seg_start;
- const char *lead;
-
- if (width < 1)
- width = 1;
- do {
- l = line_new(u);
- col = 0;
- lead = i == 0 ? prefix : continuation;
- if (lead != NULL) {
- int pw = 0;
- size_t k;
-
- for (k = 0; lead[k] != '\0'; k++)
- pw++;
- line_add(l, attr, lead, strlen(lead), pw);
- col = pw;
- }
- seg_start = i;
- last_break = (size_t)-1;
- {
- struct buf seg;
- int segw = 0;
-
- buf_init(&seg);
- while (i < len) {
- uint32_t cp;
- size_t adv;
- int w;
-
- adv = utf8_decode((const unsigned char *)text + i,
- len - i, &cp);
- if (cp == '\n') { /* hard break */
- i += adv;
- break;
- }
- w = utf8_width(cp);
- if (w < 0)
- w = 0;
- if (col + w > width && seg.len > 0) {
- /* wrap: prefer the last space */
- if (last_break != (size_t)-1 &&
- last_break > seg_start) {
- size_t back = i - last_break;
-
- seg.len -= back; /* trim */
- i = last_break;
- /* skip the space */
- i += utf8_decode(
- (const unsigned char *)text +
- i, len - i, &cp);
- }
- break;
- }
- buf_add(&seg, text + i, adv);
- segw += w;
- if (cp == ' ')
- last_break = i;
- i += adv;
- col += w;
- }
- if (seg.len > 0)
- line_add(l, attr, (char *)seg.data, seg.len,
- segw);
- buf_free(&seg);
- }
- } while (i < len);
-}
-
/* The transcript wrap width: an open drawer narrows it (2.1). */
static int
wrap_width(const struct ui *u)
attr = u->color ? COLOR_PAIR(P_TOOL) :
A_BOLD | A_UNDERLINE;
}
- render_plain(u, text + i, j - i, width, attr, NULL, NULL);
+ ui_layout_plain(&u->layout, text + i, j - i, width, attr,
+ NULL, NULL, 0);
i = (j < len) ? j + 1 : j;
}
}
md_render((const char *)it->raw.data, it->raw.len, width, u->ascii,
&d);
for (i = 0; i < d.nlines; i++) {
- struct dline *l = line_new(u);
+ struct ui_line *l = ui_layout_line(&u->layout);
for (j = 0; j < d.lines[i].nspans; j++) {
struct md_span *s = &d.lines[i].spans[j];
- line_add(l, md_attr(u, s->attr), s->text, s->len,
+ ui_layout_span(l, md_attr(u, s->attr), s->text, s->len,
s->width);
}
}
size_t i;
int w = wrap_width(u);
- lines_reset(u);
+ ui_layout_reset(&u->layout);
u->clear_line = 0;
for (i = 0; i < u->nitems; i++) {
struct titem *it = &u->items[i];
switch (it->kind) {
case IT_USER:
- render_plain(u, t, n, w,
- A_BOLD, "> ", NULL);
+ ui_layout_plain(&u->layout, t, n, w,
+ ui_owner_attr(u->color), "> ", " ", 1);
break;
case IT_ASSIST:
render_assist(u, it, w);
break;
case IT_TOOL:
- render_plain(u, t, n, w,
+ ui_layout_plain(&u->layout, t, n, w,
u->color ? COLOR_PAIR(P_TOOL) : A_DIM,
- " tool: ", " ");
+ " tool: ", " ", 0);
break;
case IT_DIFF:
render_diff(u, t, n, w);
break;
case IT_NOTE:
- render_plain(u, t, n, w,
- u->color ? COLOR_PAIR(P_DIM) : A_DIM, NULL, NULL);
+ ui_layout_plain(&u->layout, t, n, w,
+ u->color ? COLOR_PAIR(P_DIM) : A_DIM, NULL, NULL, 0);
break;
case IT_ERROR:
- render_plain(u, t, n, w,
- u->color ? COLOR_PAIR(P_ERR) : A_BOLD, "! ", NULL);
+ ui_layout_plain(&u->layout, t, n, w,
+ u->color ? COLOR_PAIR(P_ERR) : A_BOLD, "! ", NULL,
+ 0);
break;
case IT_CLEAR:
- u->clear_line = (int)u->nlines;
+ u->clear_line = (int)u->layout.nlines;
break;
}
}
}
static void
-draw_line(const struct ui *u, int row, const struct dline *l, int xoff)
+draw_line(const struct ui *u, int row, const struct ui_line *l, int xoff)
{
size_t i;
- int col = xoff;
+ int col = xoff, fill;
- for (i = 0; i < l->n && col < u->cols; i++) {
- struct span *s = &l->spans[i];
+ fill = ui_layout_visible_fill(l, u->cols, xoff);
+ if (fill > 0) {
+ move(row, xoff);
+ attrset(l->fill_attr);
+ hline(' ', fill);
+ }
+ for (i = 0; i < l->nspans && col < u->cols; i++) {
+ struct ui_span *s = &l->spans[i];
move(row, col);
attrset(s->attr);
struct buf disp;
size_t ccol = ed_cursor_col(&u->ed);
size_t start = 0, i;
- int curscol;
+ int attr, curscol;
buf_init(&disp);
ed_display(&u->ed, &disp);
start = ccol - (u->cols - 1);
curscol = (int)(ccol - start);
+ attr = ui_owner_attr(u->color);
move(row, 0);
- clrtoeol();
+ attrset(attr);
+ hline(' ', u->cols);
i = 0;
{
size_t scanned = 0;
curscol = 0;
if (curscol > u->cols - 1)
curscol = u->cols - 1;
+ attrset(A_NORMAL);
move(row, curscol); /* leave the hardware cursor here */
}
static int
tail_top(const struct ui *u, int height)
{
- int top = (int)u->nlines - height;
+ int top = (int)u->layout.nlines - height;
if (top < u->clear_line)
top = u->clear_line;
top = u->scroll;
if (top < 0)
top = 0;
- if (top > (int)u->nlines)
- top = (int)u->nlines;
+ if (top > (int)u->layout.nlines)
+ top = (int)u->layout.nlines;
erase();
for (r = 0; r < th; r++) {
int li = top + r;
- if (li >= 0 && li < (int)u->nlines)
- draw_line(u, r, &u->lines[li], xoff);
+ if (li >= 0 && li < (int)u->layout.nlines)
+ draw_line(u, r, &u->layout.lines[li], xoff);
}
draw_input(u, th);
draw_queue(u, th + 1);
{
if (UI == NULL)
return;
+ ui_layout_free(&UI->layout);
terminal_modes(0);
endwin();
UI = NULL;
noecho();
nonl();
curs_set(1);
- if (has_colors()) {
- start_color();
- use_default_colors();
- init_pair(P_HEAD, COLOR_YELLOW, -1);
- init_pair(P_CODE, COLOR_CYAN, -1);
- init_pair(P_ADD, COLOR_GREEN, -1);
- init_pair(P_DEL, COLOR_RED, -1);
- init_pair(P_TOOL, COLOR_CYAN, -1);
- init_pair(P_ERR, COLOR_RED, -1);
- init_pair(P_DIM, COLOR_BLUE, -1);
- init_pair(P_TEXT, COLOR_WHITE, -1);
- }
+ if (has_colors())
+ ui_palette_init();
terminal_modes(1);
u = xcalloc(1, sizeof(*u));
blob - /dev/null
blob + 7e7e63c09138ee4872a6f61b1188b07b44368e93 (mode 644)
--- /dev/null
+++ src/fugu-tty/ui_layout.c
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * 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.
+ */
+
+#include <sys/types.h>
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "buf.h"
+#include "utf8.h"
+#include "xmalloc.h"
+#include "ui_layout.h"
+
+/* Width of the next visible cell, skipping leading combining characters. */
+static int
+next_cell_width(const unsigned char *text, size_t len)
+{
+ uint32_t cp;
+ size_t adv, i = 0;
+ int w;
+
+ while (i < len) {
+ adv = utf8_decode(text + i, len - i, &cp);
+ if (cp == '\n')
+ return (0);
+ w = utf8_width(cp);
+ if (w > 0)
+ return (w);
+ i += adv;
+ }
+ return (0);
+}
+
+void
+ui_layout_reset(struct ui_layout *layout)
+{
+ size_t i, j;
+
+ for (i = 0; i < layout->nlines; i++) {
+ for (j = 0; j < layout->lines[i].nspans; j++)
+ free(layout->lines[i].spans[j].text);
+ free(layout->lines[i].spans);
+ }
+ layout->nlines = 0;
+}
+
+void
+ui_layout_free(struct ui_layout *layout)
+{
+ ui_layout_reset(layout);
+ free(layout->lines);
+ memset(layout, 0, sizeof(*layout));
+}
+
+struct ui_line *
+ui_layout_line(struct ui_layout *layout)
+{
+ struct ui_line *line;
+
+ if (layout->nlines == layout->cap) {
+ layout->cap = layout->cap ? layout->cap * 2 : 128;
+ layout->lines = xreallocarray(layout->lines, layout->cap,
+ sizeof(*layout->lines));
+ }
+ line = &layout->lines[layout->nlines++];
+ memset(line, 0, sizeof(*line));
+ return (line);
+}
+
+void
+ui_layout_span(struct ui_line *line, int attr, const void *text, size_t len,
+ int width)
+{
+ struct ui_span *span;
+
+ line->spans = xreallocarray(line->spans, line->nspans + 1,
+ sizeof(*line->spans));
+ span = &line->spans[line->nspans++];
+ span->attr = attr;
+ span->width = width;
+ span->len = len;
+ span->text = xmalloc(len + 1);
+ memcpy(span->text, text, len);
+ span->text[len] = '\0';
+}
+
+/* Wrap plain UTF-8 text to width columns, one attribute, into new lines. */
+void
+ui_layout_plain(struct ui_layout *layout, const void *vtext, size_t len,
+ int width, int attr, const char *prefix, const char *continuation,
+ int fill)
+{
+ const unsigned char *text = vtext;
+ struct ui_line *line;
+ size_t i = 0;
+ int col, first = 1;
+ const char *lead;
+
+ if (width < 1)
+ width = 1;
+ do {
+ struct buf seg;
+ size_t last_break, break_len, leadlen;
+ int segw, break_width, lead_limit, need;
+
+ line = ui_layout_line(layout);
+ if (fill) {
+ line->fill_attr = attr;
+ line->fill_width = width;
+ }
+ col = 0;
+ lead = first ? prefix : continuation;
+ if (lead != NULL) {
+ leadlen = strlen(lead);
+ lead_limit = width;
+ if (!first && i < len) {
+ need = next_cell_width(text + i, len - i);
+ if (need > width)
+ need = width;
+ if (need > 0)
+ lead_limit = width - need;
+ }
+ if (leadlen > (size_t)lead_limit)
+ leadlen = (size_t)lead_limit;
+
+ if (leadlen > 0) {
+ ui_layout_span(line, attr, lead, leadlen,
+ (int)leadlen);
+ col = (int)leadlen;
+ }
+ }
+ first = 0;
+
+ buf_init(&seg);
+ segw = 0;
+ last_break = (size_t)-1;
+ break_len = 0;
+ break_width = 0;
+ while (i < len) {
+ uint32_t cp;
+ size_t adv;
+ int w;
+
+ adv = utf8_decode(text + i, len - i, &cp);
+ if (cp == '\n') {
+ i += adv;
+ break;
+ }
+ w = utf8_width(cp);
+ if (w < 0)
+ w = 0;
+ if (col + w > width) {
+ if (segw > 0) {
+ if (last_break != (size_t)-1) {
+ seg.len = break_len;
+ segw = break_width;
+ i = last_break;
+ i += utf8_decode(text + i,
+ len - i, &cp);
+ }
+ break;
+ }
+ if (col > 0)
+ break;
+ /* One glyph cannot fit this pane at all. */
+ buf_addc(&seg, '?');
+ segw = col = 1;
+ i += adv;
+ continue;
+ }
+ buf_add(&seg, text + i, adv);
+ segw += w;
+ col += w;
+ if (cp == ' ' && seg.len > adv) {
+ last_break = i;
+ break_len = seg.len - adv;
+ break_width = segw - w;
+ }
+ i += adv;
+ }
+ if (seg.len > 0)
+ ui_layout_span(line, attr, seg.data, seg.len, segw);
+ buf_free(&seg);
+ } while (i < len);
+}
+
+int
+ui_layout_visible_fill(const struct ui_line *line, int cols, int xoff)
+{
+ int available, fill;
+
+ available = cols - xoff;
+ if (available <= 0 || line->fill_width <= 0)
+ return (0);
+ fill = line->fill_width;
+ if (fill > available)
+ fill = available;
+ return (fill);
+}
blob - /dev/null
blob + 85c490f435a7c16cfa3993077783f5fb53324520 (mode 644)
--- /dev/null
+++ src/fugu-tty/ui_layout.h
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * 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.
+ */
+
+#ifndef UI_LAYOUT_H
+#define UI_LAYOUT_H
+
+#include <stddef.h>
+
+struct ui_span {
+ int attr;
+ char *text;
+ size_t len;
+ int width;
+};
+
+struct ui_line {
+ struct ui_span *spans;
+ size_t nspans;
+ int fill_attr;
+ int fill_width;
+};
+
+struct ui_layout {
+ struct ui_line *lines;
+ size_t nlines;
+ size_t cap;
+};
+
+void ui_layout_reset(struct ui_layout *);
+void ui_layout_free(struct ui_layout *);
+struct ui_line *ui_layout_line(struct ui_layout *);
+void ui_layout_span(struct ui_line *, int, const void *, size_t,
+ int);
+void ui_layout_plain(struct ui_layout *, const void *, size_t, int,
+ int, const char *, const char *, int);
+int ui_layout_visible_fill(const struct ui_line *, int, int);
+
+#endif /* UI_LAYOUT_H */
blob - /dev/null
blob + e7d9863b6f3d1952791f34d8555e14c3e2822a62 (mode 644)
--- /dev/null
+++ src/fugu-tty/ui_palette.c
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * 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.
+ */
+
+#include <curses.h>
+
+#include "ui_palette.h"
+
+void
+ui_palette_init(void)
+{
+ start_color();
+ use_default_colors();
+ init_pair(P_HEAD, COLOR_YELLOW, -1);
+ init_pair(P_CODE, COLOR_CYAN, -1);
+ init_pair(P_ADD, COLOR_GREEN, -1);
+ init_pair(P_DEL, COLOR_RED, -1);
+ init_pair(P_TOOL, COLOR_CYAN, -1);
+ init_pair(P_ERR, COLOR_RED, -1);
+ init_pair(P_DIM, COLOR_BLUE, -1);
+ init_pair(P_TEXT, COLOR_WHITE, -1);
+ init_pair(P_OWNER, COLOR_BLACK, COLOR_WHITE);
+}
+
+int
+ui_owner_attr(int color)
+{
+ return (A_BOLD | (color ? COLOR_PAIR(P_OWNER) : A_REVERSE));
+}
blob - /dev/null
blob + e49ac5a61b752f0d1b47c30dd6c30d32e7aede06 (mode 644)
--- /dev/null
+++ src/fugu-tty/ui_palette.h
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * 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.
+ */
+
+#ifndef UI_PALETTE_H
+#define UI_PALETTE_H
+
+enum ui_pair {
+ P_HEAD = 1,
+ P_CODE,
+ P_ADD,
+ P_DEL,
+ P_TOOL,
+ P_ERR,
+ P_DIM,
+ P_TEXT,
+ P_OWNER
+};
+
+void ui_palette_init(void);
+int ui_owner_attr(int);
+
+#endif /* UI_PALETTE_H */