commit - b10a8c42ca51a9080cc5df95ecd26eee613eaf87
commit + 9a9cb3b0fefe2093cd34035349db0fcb8e569ba6
blob - ab234efe25ba7c9fe57c68c6dd9950036920e21a
blob + 789d2e7baac27673436064c8a4bb921bb95b87c4
--- docs/design/m7-curses.md
+++ docs/design/m7-curses.md
*styled display lines* (attribute runs over UTF-8 cells), with
wide/combining-character width, fenced code blocks, headings,
bullets, inline code, bold/italic, unmatched markers literal, and an
- `ascii` fold (typographic punctuation to ASCII, unknown glyphs to
+ shared `ascii.c` fold (typographic punctuation to ASCII, unknown glyphs to
`?`, markdown structure preserved). Pure; `regress/markdown`.
- **editor.c** — the modal vi-like line editor over a byte buffer:
insert/normal modes, the motion/delete verbs, history recall, the
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.
+ 64 KiB cap), slash-completion cycling, and presentation-only ASCII folding
+ with matching cursor geometry. Pure; `regress/editor` drives key-op
+ sequences and asserts buffer, cursor, display, and submission.
+- **ui_layout.c** — transcript rows as attributed UTF-8 spans, an optional
+ fill attribute and pane width, and normalization of malformed/control bytes
+ to visible Unicode cells. Pure; `regress/ui_layout` pins User prefixes,
+ continuation indentation, hostile bytes, ASCII expansion, word and
+ wide/combining-character wrapping, narrow panes and picker overlays, drawer
+ clipping, and resize reflow before the wide curses interface paints them.
- **welcome.c** — the empty-transcript welcome's one-way dismissal state,
exact copy, viewport layout, ASCII fold, and status-caption collision
policy. Pure; `regress/welcome` proves that resize and temporary transcript
blob - 857a9356989627994510c409c83cc2fb50b15f83
blob + b483a31281f5a4eb246f31778a66d7c4a277086e
--- handoff/verification.md
+++ handoff/verification.md
- **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.
+ visible normalization of NUL/control/malformed bytes, ASCII transliteration
+ and matching input-cursor width, narrow-pane fallback, drawer clipping, and
+ resize reflow. The curses pty path exercises the styled input and transcript
+ through wide curses while retaining clean terminal-mode startup and teardown.
- **Empty-transcript welcome:** a pure suite pins the exact copy, centering in
full and drawer-narrowed viewports, ASCII punctuation, whole-row omission on
short/narrow terminals, nonoverlapping status captions, and one-way
- dismissal. Staged curses pty snapshots prove startup visibility and removal
- on the first nonempty edit, as well as absence from the Provider request and
- Journal and suppression of the default and named Claude-profile warning only
- in curses mode. Line/print regressions retain the stderr warning.
+ dismissal. Pure geometry checks keep tiny picker overlays above the status
+ row. Staged curses pty repaint captures prove startup visibility, restoration
+ after a presentation-only model selection, and, after a forced physical
+ clear, removal on the first nonempty edit. They also prove absence from the
+ Provider request and Journal and suppression of the default and named
+ Claude-profile warning only in curses mode. Line/print regressions retain the
+ stderr warning.
- **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 - ec86d3a9f8a8a8b6fec32b089294ebededae6004
blob + feef42d0e897dca0bb6064c1bcbf51cef1fd1623
--- regress/README
+++ regress/README
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
+ hostile-byte normalization, ASCII fold,
+ UTF-8 and reflow); editor (folded input
+ cursor); turn (monochrome pty
startup and teardown)
empty-transcript welcome (2.1) welcome (copy, layout, ASCII, caption
collision, one-way dismissal); turn (pty,
blob - 1b7816836acb53a114befc361938f11f5d6551df
blob + d4f8554a7ecabc3f5e3c9109a06ddc5e9c486367
--- regress/editor/Makefile
+++ regress/editor/Makefile
PROG= editor_test
-SRCS= editor_test.c editor.c utf8.c buf.c log.c xmalloc.c
+SRCS= editor_test.c editor.c ascii.c utf8.c buf.c log.c xmalloc.c
NOMAN= yes
CFLAGS+= -I${.CURDIR}/../../src/fugu-tty
blob - aca4847ad372cc198d61a8a0a60045a7772aff96
blob + 61a0c8af47da23239630abbd89df054cdd2959d8
--- regress/editor/editor_test.c
+++ regress/editor/editor_test.c
int ok;
buf_init(&b);
- ed_display(e, &b);
+ ed_display(e, &b, 0);
buf_addc(&b, '\0');
ok = strcmp((const char *)b.data, want) == 0;
buf_free(&b);
CHECK(text_is(&e, "ab")); /* ESC stripped */
ed_free(&e);
+ /* Controls draw as one visible replacement cell; the cursor agrees. */
ed_init(&e);
+ ed_feed(&e, K_PASTE, 0, "A\tB\xc2\x85" "C", 6);
+ CHECK(ed_cursor_col(&e, 0) == 5);
{
+ struct buf b;
+
+ buf_init(&b);
+ ed_display(&e, &b, 1);
+ CHECK(b.len == 5 && memcmp(b.data, "A B?C", 5) == 0);
+ buf_free(&b);
+ }
+ ed_free(&e);
+
+ /* ASCII mode folds display only and counts expansion at the cursor. */
+ ed_init(&e);
+ ed_feed(&e, K_PASTE, 0,
+ "A\xe2\x80\xa6\xe2\x80\x94\xe4\xb8\xad", 10);
+ {
+ struct buf b;
+
+ buf_init(&b);
+ ed_display(&e, &b, 1);
+ CHECK(b.len == 7 && memcmp(b.data, "A...--?", 7) == 0);
+ buf_free(&b);
+ }
+ CHECK(ed_cursor_col(&e, 1) == 7);
+ CHECK(text_is(&e,
+ "A\xe2\x80\xa6\xe2\x80\x94\xe4\xb8\xad"));
+ ed_free(&e);
+
+ ed_init(&e);
+ {
static char big[70000];
memset(big, 'x', sizeof(big));
blob - a06f5d6422d99ef3616b6a456c6658c5a837b351
blob + 46205337201d56ce7feedcbc414d01d73b0d76bc
--- regress/markdown/Makefile
+++ regress/markdown/Makefile
PROG= markdown_test
-SRCS= markdown_test.c markdown.c utf8.c buf.c log.c xmalloc.c
+SRCS= markdown_test.c markdown.c ascii.c utf8.c buf.c log.c xmalloc.c
NOMAN= yes
CFLAGS+= -I${.CURDIR}/../../src/fugu-tty
blob - 16f347790f6041328417b70fb5cef7787808e526
blob + 610dddc751b30b0639fcad85e0bb5243fe807f14
--- regress/turn/run.sh
+++ regress/turn/run.sh
rm -rf "$HOME/.fugu"
start_stub "CONTEXT"
write_conf "$stubport"
- printf '\000\033o\000\033\014\000m\000\000\033\014\000iO\000WNER-BLOCK-MARKER\r/quit\r' | \
+ printf '\000\033o\000\033\014\000m\000\000\r\014\000m\000\000\033\014\000iOWNER-BLOCK-MARKER\014\000\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 \
test -n "$picker_stage"
ok "curses: model picker overlays the undismissed welcome" $?
grep -a -Fq "I like Fugu" "$dir/welcome-stage.5" &&
- grep -a -Fq "context used / limit" "$dir/welcome-stage.5"
- ok "curses: closing the model picker restores the welcome" $?
- test -s "$dir/welcome-stage.6" &&
- ! grep -a -Fq "I like Fugu" "$dir/welcome-stage.6" &&
- ! grep -a -Fq "agent state" "$dir/welcome-stage.6" &&
- ! grep -a -Fq "context used / limit" "$dir/welcome-stage.6"
- ok "curses: first nonempty edit removes welcome and captions" $?
- grep -a -Fq "Context 46k/200k 23%" "$dir/cursesout"
+ grep -a -Fq "context used / limit" "$dir/welcome-stage.5" &&
+ ! grep -a -Fq "model: stub-model-a" "$dir/welcome-stage.5"
+ ok "curses: selecting from m restores welcome without scrollback" $?
+ picker_stage=''
+ for stage in 6 7; do
+ if grep -a -Fq "stub-model-b" "$dir/welcome-stage.$stage"; then
+ picker_stage=$stage
+ fi
+ done
+ test -n "$picker_stage"
+ ok "curses: model picker can reopen before Owner input" $?
+ grep -a -Fq "I like Fugu" "$dir/welcome-stage.8" &&
+ grep -a -Fq "context used / limit" "$dir/welcome-stage.8"
+ ok "curses: cancelling the model picker restores the welcome" $?
+ test -s "$dir/welcome-stage.9" &&
+ grep -a -Fq "OWNER-BLOCK-MARKER" "$dir/welcome-stage.9" &&
+ ! grep -a -Fq "I like Fugu" "$dir/welcome-stage.9" &&
+ ! grep -a -Fq "agent state" "$dir/welcome-stage.9" &&
+ ! grep -a -Fq "context used / limit" "$dir/welcome-stage.9"
+ ok "curses: forced repaint after first edit omits welcome and captions" $?
+ grep -q '"model":"stub-model-a"' "$dir/reqout" 2>/dev/null
+ ok "curses: pre-input picker selection drives the request" $?
+ grep -a -Fq "Context 46k/111k 41%" "$dir/cursesout"
ok "curses: status shows Lead cache plus session context" $?
grep -q "I like Fugu" "$dir/reqout" 2>/dev/null
ok "curses: welcome is absent from the Provider request" \
blob - d5e67a6bd303a05123ca0616e4956ac8995b29a3
blob + d2c19e6eaf4259c491ac735405b37fcf6553b371
--- regress/ui_layout/Makefile
+++ 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
+SRCS= ui_layout_test.c ui_layout.c ui_palette.c ascii.c utf8.c buf.c \
+ log.c xmalloc.c
NOMAN= yes
CFLAGS+= -I${.CURDIR}/../../src/fugu-tty
blob - 5d8fdb1ce2fe947b2e679eaf92e1ee92cc7b8952
blob + cef615c14a41843149a5070594aec7c507885bd8
--- regress/ui_layout/ui_layout_test.c
+++ regress/ui_layout/ui_layout_test.c
"A\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd"
"\xef\xbf\xbd(\xef\xbf\xbd(B";
struct ui_layout layout = { 0 };
+ struct ui_picker_layout picker;
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);
+ ui_layout_plain(&layout, "hello", 5, 10, attr, "> ", " ", 1, 0);
CHECK(layout.nlines == 1);
CHECK(line_is(&layout.lines[0], "> hello"));
CHECK(layout.lines[0].fill_width == 10);
CHECK(ui_layout_visible_fill(&layout.lines[0], 8, 3) == 5);
CHECK(ui_layout_visible_fill(&layout.lines[0], 3, 3) == 0);
+ /* Tiny picker overlays never reach the input/status rows below them. */
+ ui_layout_picker(&picker, 3, 20);
+ CHECK(picker.top == 0 && picker.height == 1 &&
+ picker.model_rows == 0 && picker.top + picker.height <= 3 - 2);
+ ui_layout_picker(&picker, 4, 20);
+ CHECK(picker.top == 0 && picker.height == 2 &&
+ picker.model_rows == 0 && picker.top + picker.height <= 4 - 2);
+ ui_layout_picker(&picker, 5, 20);
+ CHECK(picker.top == 0 && picker.height == 3 &&
+ picker.model_rows == 0 && picker.top + picker.height <= 5 - 2);
+ ui_layout_picker(&picker, 6, 20);
+ CHECK(picker.top == 0 && picker.height == 4 &&
+ picker.model_rows == 1 && picker.top + picker.height <= 6 - 2);
+ ui_layout_picker(&picker, 40, 2);
+ CHECK(picker.top == 17 && picker.height == 5 &&
+ picker.model_rows == 2 && picker.top + picker.height <= 40 - 2);
+
/* 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);
+ ui_layout_plain(&layout, "abcdefghi", 9, 6, attr, "> ", " ", 1, 0);
CHECK(layout.nlines == 3);
CHECK(line_is(&layout.lines[0], "> abcd"));
CHECK(line_is(&layout.lines[1], " efgh"));
/* 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);
+ ui_layout_plain(&layout, "a\n\nb", 4, 8, attr, "> ", " ", 1, 0);
CHECK(layout.nlines == 3);
CHECK(line_is(&layout.lines[0], "> a"));
CHECK(line_is(&layout.lines[1], " "));
/* 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);
+ ui_layout_plain(&layout, "one two", 7, 8, attr, "> ", " ", 1, 0);
CHECK(layout.nlines == 2);
CHECK(line_is(&layout.lines[0], "> one"));
CHECK(layout.lines[0].nspans == 2 &&
/* 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);
+ 6, attr, "> ", " ", 1, 0);
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);
+ /* ASCII folding precedes wrapping and applies to Owner rows. */
+ ui_layout_reset(&layout);
+ ui_layout_plain(&layout, "\xe2\x80\x94\xe4\xb8\xad", 6, 4,
+ attr, "> ", " ", 1, 1);
+ CHECK(layout.nlines == 2);
+ CHECK(line_is(&layout.lines[0], "> --"));
+ CHECK(line_is(&layout.lines[1], " ?"));
+ CHECK(line_width(&layout.lines[0]) == 4 &&
+ line_width(&layout.lines[1]) == 3);
+
/*
* Hostile transcript bytes become visible UTF-8 cells before storage:
* embedded NUL, C0/C1 controls, and each malformed byte are U+FFFD.
*/
ui_layout_reset(&layout);
ui_layout_plain(&layout, hostile, sizeof(hostile), 40, attr, NULL,
- NULL, 0);
+ NULL, 0, 0);
CHECK(layout.nlines == 1);
CHECK(line_bytes_are(&layout.lines[0], normalized,
sizeof(normalized) - 1));
/* Re-rendering at a narrower pane pins resize and drawer reflow. */
ui_layout_reset(&layout);
- ui_layout_plain(&layout, "abcdef", 6, 5, attr, "> ", " ", 1);
+ ui_layout_plain(&layout, "abcdef", 6, 5, attr, "> ", " ", 1, 0);
CHECK(layout.nlines == 2 && layout.lines[0].fill_width == 5);
ui_layout_reset(&layout);
- ui_layout_plain(&layout, "abcdef", 6, 4, attr, "> ", " ", 1);
+ ui_layout_plain(&layout, "abcdef", 6, 4, attr, "> ", " ", 1, 0);
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);
+ ui_layout_plain(&layout, "ab", 2, 1, attr, "> ", " ", 1, 0);
CHECK(layout.nlines == 3);
CHECK(line_is(&layout.lines[0], ">"));
CHECK(line_is(&layout.lines[1], "a"));
line_width(&layout.lines[2]) <= 1);
ui_layout_reset(&layout);
- ui_layout_plain(&layout, "ab", 2, 2, attr, "> ", " ", 1);
+ ui_layout_plain(&layout, "ab", 2, 2, attr, "> ", " ", 1, 0);
CHECK(layout.nlines == 3);
CHECK(line_is(&layout.lines[0], "> "));
CHECK(line_is(&layout.lines[1], " a"));
/* 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);
+ ui_layout_plain(&layout, "\xe4\xb8\xad", 3, 1, attr, "> ", " ", 1,
+ 0);
CHECK(layout.nlines == 2 && line_is(&layout.lines[0], ">") &&
line_is(&layout.lines[1], "?"));
CHECK(line_width(&layout.lines[0]) == 1 &&
/* Other transcript kinds retain ordinary, unfilled lines. */
ui_layout_reset(&layout);
- ui_layout_plain(&layout, "assistant", 9, 20, 7, NULL, NULL, 0);
+ ui_layout_plain(&layout, "assistant", 9, 20, 7, NULL, NULL, 0, 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);
+ ui_layout_plain(&layout, NULL, 0, 12, attr, "> ", " ", 1, 0);
CHECK(layout.nlines == 1 && line_is(&layout.lines[0], "> "));
CHECK(layout.lines[0].fill_width == 12);
blob - 9a92356bbf6c44202ee32acb8d09452d52504700
blob + 1bb802445df759a57bc8d38fc2c0dfa2fa850466
--- src/fugu/coord.c
+++ src/fugu/coord.c
struct listed_model *listed_models; /* validates picker selections */
size_t nlisted_models;
int models_collecting;
+ int models_announce; /* slash-command feedback */
int default_provider_type; /* to restore on slot 0 */
int active_provider_slot;
int64_t provider_window; /* selected listing metadata */
return (NULL);
}
-/* Start one live model listing; normal-mode `m` keeps it presentation-only. */
+/* Start one live model listing; normal-mode "m" stays presentation-only. */
static void
model_list_begin(struct coord *c, int announce)
{
c->listed_models = NULL;
c->nlisted_models = 0;
c->models_collecting = 1;
+ c->models_announce = announce;
imsgev_send(&c->iev[ROLE_API], FUGU_IMSG_LIST_MODELS, 0, NULL, 0);
if (announce)
emit_note(c, "fetching the model list...");
turn_txn_cache_reset(c->turn); /* the cache starts cold (8) */
context_invalidate(c); /* tokenization/provider changed */
send_ui_config(c); /* the status bar shows it */
- emit_note(c, "model: %s", id);
+ if (c->models_announce)
+ emit_note(c, "model: %s", id);
+ c->models_announce = 0;
break;
}
}
blob - 3d338f1558d354235e859836b6c17d91327b78e2
blob + 653d4cb2e00b5fcbb159c201000a25766325e20f
--- src/fugu-tty/Makefile
+++ src/fugu-tty/Makefile
PROG= fugu-tty
SRCS= main.c ui.c ui_config_rx.c ui_layout.c ui_palette.c welcome.c \
- markdown.c editor.c term_input.c utf8.c worker.c imsgev.c buf.c log.c \
- xmalloc.c
+ markdown.c editor.c ascii.c term_input.c utf8.c worker.c imsgev.c \
+ buf.c log.c xmalloc.c
BINDIR= ${LIBEXECDIR}/fugu
MAN=
blob - 7ceca5556b0eebac5415e0f3df17f4faba18826d
blob + 21d6e3e8a798360c3f4b51b95c9fd51bba948c8c
--- src/fugu-tty/editor.c
+++ src/fugu-tty/editor.c
#include <stdlib.h>
#include <string.h>
+#include "ascii.h"
#include "xmalloc.h"
#include "buf.h"
#include "utf8.h"
return ((const char *)textbuf.data);
}
+/* Append one display code point (or its ASCII fold), returning its width. */
+static size_t
+display_cp(struct buf *out, uint32_t cp, int ascii)
+{
+ unsigned char enc[4];
+ uint32_t folded[4];
+ size_t enclen, width = 0;
+ int i, n, w;
+
+ if (ascii)
+ n = ascii_fold_cp(cp, folded);
+ else {
+ folded[0] = cp;
+ n = 1;
+ }
+ for (i = 0; i < n; i++) {
+ w = utf8_width(folded[i]);
+ if (folded[i] == 0 || w < 0)
+ w = 1; /* controls render as one visible cell */
+ width += (size_t)w;
+ if (out != NULL) {
+ enclen = utf8_encode(folded[i], enc);
+ buf_add(out, enc, enclen);
+ }
+ }
+ return (width);
+}
+
void
-ed_display(const struct editor *e, struct buf *out)
+ed_display(const struct editor *e, struct buf *out, int ascii)
{
size_t i;
if (e->items[i].paste)
buf_addf(out, "[paste: %d lines]", e->items[i].lines);
else
- buf_add(out, e->items[i].bytes, e->items[i].nbytes);
+ (void)display_cp(out, e->items[i].cp, ascii);
}
}
size_t ed_nitems(const struct editor *e) { return (e->n); }
size_t
-ed_cursor_col(const struct editor *e)
+ed_cursor_col(const struct editor *e, int ascii)
{
size_t i, col = 0;
k = snprintf(tmp, sizeof(tmp), "[paste: %d lines]",
e->items[i].lines);
col += (k > 0 ? (size_t)k : 0);
- } else {
- int w = utf8_width(e->items[i].cp);
-
- col += (w < 0 ? 0 : (size_t)w);
- }
+ } else
+ col += display_cp(NULL, e->items[i].cp, ascii);
}
return (col);
}
blob - /dev/null
blob + 83dac1d1edf876b6e119211549cafbe56bb32023 (mode 644)
--- /dev/null
+++ src/fugu-tty/ascii.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 <stddef.h>
+#include <stdint.h>
+
+#include "ascii.h"
+
+int
+ascii_fold_cp(uint32_t cp, uint32_t out[4])
+{
+ const char *rep = NULL;
+ int i, n;
+
+ if (cp == '\n') {
+ out[0] = cp; /* structural line break */
+ return (1);
+ }
+ if (cp == '\t') {
+ out[0] = ' ';
+ return (1);
+ }
+ if (cp < 0x20 || cp == 0x7f) {
+ out[0] = '?';
+ return (1);
+ }
+ if (cp < 0x80) {
+ out[0] = cp;
+ return (1);
+ }
+ switch (cp) {
+ case 0x2018: case 0x2019: case 0x2032: out[0] = '\''; return (1);
+ case 0x201C: case 0x201D: case 0x2033: out[0] = '"'; return (1);
+ case 0x2013: out[0] = '-'; return (1);
+ case 0x00A0: out[0] = ' '; return (1);
+ case 0x2022: case 0x00B7: case 0x2023: out[0] = '*'; return (1);
+ case 0x2014: rep = "--"; break;
+ case 0x2026: rep = "..."; break;
+ case 0x00A9: rep = "(c)"; break;
+ case 0x00AE: rep = "(r)"; break;
+ case 0x2122: rep = "(tm)"; break;
+ case 0x2192: rep = "->"; break;
+ case 0x2190: rep = "<-"; break;
+ default: out[0] = '?'; return (1);
+ }
+ for (i = 0, n = 0; rep[i] != '\0' && n < 4; i++)
+ out[n++] = (unsigned char)rep[i];
+ return (n);
+}
blob - c053afb616e32c0c496b53e6e50b136e60a78c14
blob + ca7d21a4814bda6c99255dbf3542ff88740295b2
--- src/fugu-tty/editor.h
+++ src/fugu-tty/editor.h
int ed_mode(const struct editor *);
size_t ed_cursor(const struct editor *);
size_t ed_nitems(const struct editor *);
-/* Display column of the cursor in the rendered line (paste placeholders
- * and wide characters counted), for the curses layer to place it. */
-size_t ed_cursor_col(const struct editor *);
+/* Display column of the cursor in the rendered line (paste placeholders,
+ * ASCII folds, and wide characters counted), for curses to place it. */
+size_t ed_cursor_col(const struct editor *, int ascii);
void ed_history_add(struct editor *, const char *, size_t);
void ed_set_completions(struct editor *, char **names, size_t n);
-/* The display string with paste placeholders; for the driver and tests. */
-void ed_display(const struct editor *, struct buf *);
+/* The display string with paste placeholders and optional ASCII folding. */
+void ed_display(const struct editor *, struct buf *, int ascii);
/* A one-shot notice (oversize paste, &c.); NULL if none pending. */
const char *ed_take_notice(struct editor *);
blob - /dev/null
blob + aca64e010fcfcd99a51593168e8e188dbb8d6e84 (mode 644)
--- /dev/null
+++ src/fugu-tty/ascii.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 ASCII_H
+#define ASCII_H
+
+#include <stdint.h>
+
+/*
+ * Map one code point to a readable ASCII stand-in of at most four cells.
+ * Printable ASCII and newlines pass through, tabs become spaces, and unknown
+ * or nonprinting glyphs become '?'.
+ */
+int ascii_fold_cp(uint32_t, uint32_t [4]);
+
+#endif /* ASCII_H */
blob - 0a218495e86efd4ffbb6c4a34691a14ced2164e5
blob + 5a2ac48675c6d5851fb83e62bb908d17770ad739
--- src/fugu-tty/markdown.c
+++ src/fugu-tty/markdown.c
#include <stdlib.h>
#include <string.h>
+#include "ascii.h"
#include "xmalloc.h"
#include "buf.h"
#include "utf8.h"
return (ln);
}
-/* ---- ascii fold ----------------------------------------------------- */
-
-/*
- * Under `ascii yes` map a code point to a short ASCII stand-in sequence
- * (out holds up to 4), preserving readability; any other non-ASCII glyph
- * becomes '?'. Returns the count. ASCII passes through unchanged.
- */
-static int
-ascii_fold(uint32_t cp, uint32_t out[4])
-{
- const char *rep = NULL;
- int i, n;
-
- if (cp < 0x80) {
- out[0] = cp;
- return (1);
- }
- switch (cp) {
- case 0x2018: case 0x2019: case 0x2032: out[0] = '\''; return (1);
- case 0x201C: case 0x201D: case 0x2033: out[0] = '"'; return (1);
- case 0x2013: out[0] = '-'; return (1);
- case 0x00A0: out[0] = ' '; return (1);
- case 0x2022: case 0x00B7: case 0x2023: out[0] = '*'; return (1);
- case 0x2014: rep = "--"; break;
- case 0x2026: rep = "..."; break;
- case 0x00A9: rep = "(c)"; break;
- case 0x00AE: rep = "(r)"; break;
- case 0x2122: rep = "(tm)"; break;
- case 0x2192: rep = "->"; break;
- case 0x2190: rep = "<-"; break;
- default: out[0] = '?'; return (1);
- }
- for (i = 0, n = 0; rep[i] != '\0' && n < 4; i++)
- out[n++] = (unsigned char)rep[i];
- return (n);
-}
-
/* ---- decode a logical line into cells ------------------------------- */
static void
uint32_t out[4];
int k, m;
- m = ascii_fold(cp, out);
+ m = ascii_fold_cp(cp, out);
for (k = 0; k < m; k++)
push_cp(&cells, &n, &cap, out[k]);
} else
blob - bac1fe598e0384069d9d1cf6244497a9fab59110
blob + c3a532af8c47fa980b25808e997af51c1e3d9e80
--- src/fugu-tty/ui.c
+++ src/fugu-tty/ui.c
A_BOLD | A_UNDERLINE;
}
ui_layout_plain(&u->layout, text + i, j - i, width, attr,
- NULL, NULL, 0);
+ NULL, NULL, 0, u->ascii);
i = (j < len) ? j + 1 : j;
}
}
switch (it->kind) {
case IT_USER:
ui_layout_plain(&u->layout, t, n, w,
- ui_owner_attr(u->color), "> ", " ", 1);
+ ui_owner_attr(u->color), "> ", " ", 1, u->ascii);
break;
case IT_ASSIST:
render_assist(u, it, w);
case IT_TOOL:
ui_layout_plain(&u->layout, t, n, w,
u->color ? COLOR_PAIR(P_TOOL) : A_DIM,
- " tool: ", " ", 0);
+ " tool: ", " ", 0, u->ascii);
break;
case IT_DIFF:
render_diff(u, t, n, w);
break;
case IT_NOTE:
ui_layout_plain(&u->layout, t, n, w,
- u->color ? COLOR_PAIR(P_DIM) : A_DIM, NULL, NULL, 0);
+ u->color ? COLOR_PAIR(P_DIM) : A_DIM, NULL, NULL, 0,
+ u->ascii);
break;
case IT_ERROR:
ui_layout_plain(&u->layout, t, n, w,
u->color ? COLOR_PAIR(P_ERR) : A_BOLD, "! ", NULL,
- 0);
+ 0, u->ascii);
break;
case IT_CLEAR:
u->clear_line = (int)u->layout.nlines;
draw_input(struct ui *u, int row)
{
struct buf disp;
- size_t ccol = ed_cursor_col(&u->ed);
+ size_t ccol = ed_cursor_col(&u->ed, u->ascii);
size_t start = 0, i;
int attr, curscol;
buf_init(&disp);
- ed_display(&u->ed, &disp);
+ ed_display(&u->ed, &disp, u->ascii);
/* horizontal scroll so the cursor stays on screen */
if ((int)ccol > u->cols - 1)
break;
}
attrset(attr);
- mvaddnstr(row->row, row->col, row->text, (int)row->len);
+ move(row->row, row->col);
+ (void)draw_utf8_cells(row->text, row->len, row->width);
}
attrset(A_NORMAL);
}
static void
draw_picker(struct ui *u)
{
+ struct ui_picker_layout layout;
size_t match[256];
size_t n = pick_matches(u, match, 256);
int w = u->cols - 4 < 60 ? u->cols - 4 : 60;
if (w < 20)
w = u->cols < 20 ? u->cols : 20;
- h = (int)n + 3;
- if (h > u->rows - 2)
- h = u->rows - 2;
+ ui_layout_picker(&layout, u->rows, n);
+ h = layout.height;
+ rows = layout.model_rows;
+ if (h == 0)
+ return;
x = (u->cols - w) / 2;
- y = (u->rows - h) / 2;
+ y = layout.top;
if (u->pick_sel >= n)
u->pick_sel = n > 0 ? n - 1 : 0;
/* scroll the visible window to keep the selection on screen (2.1):
* a real /v1/models listing has far more entries than rows */
- rows = h - 3;
- if (rows < 1)
- rows = 1;
- if (u->pick_sel < u->pick_top)
- u->pick_top = u->pick_sel;
- else if (u->pick_sel >= u->pick_top + (size_t)rows)
- u->pick_top = u->pick_sel - rows + 1;
- if (u->pick_top > n)
+ if (rows == 0)
u->pick_top = 0;
+ else {
+ if (u->pick_sel < u->pick_top)
+ u->pick_top = u->pick_sel;
+ else if (u->pick_sel >= u->pick_top + (size_t)rows)
+ u->pick_top = u->pick_sel - rows + 1;
+ if (u->pick_top > n)
+ u->pick_top = 0;
+ }
for (r = 0; r < h; r++) {
move(y + r, x);
}
attrset(A_REVERSE | A_BOLD);
mvaddnstr(y, x + 1, "model", w - 2);
- attrset(A_REVERSE);
- mvprintw(y + 1, x + 1, "> %.*s", w - 4, u->filter);
+ if (h > 1) {
+ attrset(A_REVERSE);
+ mvprintw(y + 1, x + 1, "> %.*s", w - 4, u->filter);
+ }
for (r = 0; r < rows && u->pick_top + (size_t)r < n; r++) {
size_t mi = u->pick_top + (size_t)r;
struct pick *m = &u->models[match[mi]];
blob - e930cf754a8196847eda80497a1253fa5efae8c6
blob + cc5cdd88b5362488a68cf9051d3cd5574fc24c80
--- src/fugu-tty/ui_layout.c
+++ src/fugu-tty/ui_layout.c
#include <sys/types.h>
+#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
+#include "ascii.h"
#include "buf.h"
#include "utf8.h"
#include "xmalloc.h"
buf_free(&normalized);
}
+/* Fold text before wrapping so expansions participate in row geometry. */
+static void
+fold_ascii(struct buf *out, const unsigned char *text, size_t len)
+{
+ unsigned char enc[4];
+ uint32_t cp, folded[4];
+ size_t adv, enclen, i;
+ int j, n;
+
+ for (i = 0; i < len; i += adv) {
+ adv = utf8_decode(text + i, len - i, &cp);
+ n = ascii_fold_cp(cp, folded);
+ for (j = 0; j < n; j++) {
+ enclen = utf8_encode(folded[j], enc);
+ buf_add(out, enc, enclen);
+ }
+ }
+}
+
/* 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)
+ int fill, int ascii)
{
const unsigned char *text = vtext;
struct ui_line *line;
+ struct buf folded;
size_t i = 0;
int col, first = 1;
const char *lead;
+ buf_init(&folded);
+ if (ascii && len > 0) {
+ fold_ascii(&folded, text, len);
+ text = folded.data;
+ len = folded.len;
+ }
if (width < 1)
width = 1;
do {
ui_layout_span(line, attr, seg.data, seg.len);
buf_free(&seg);
} while (i < len);
+ buf_free(&folded);
}
int
fill = available;
return (fill);
}
+
+void
+ui_layout_picker(struct ui_picker_layout *layout, int screen_rows,
+ size_t matches)
+{
+ int available, desired, usable;
+
+ available = screen_rows > 2 ? screen_rows - 2 : 0;
+ usable = screen_rows > 0 ? screen_rows - 1 : 0;
+ desired = matches > (size_t)INT_MAX - 3 ? INT_MAX :
+ (int)matches + 3; /* title, filter, bottom padding */
+ layout->height = desired < available ? desired : available;
+ layout->top = (usable - layout->height) / 2;
+ layout->model_rows = layout->height > 3 ? layout->height - 3 : 0;
+}
blob - 2ca324091c9504b1a0b68bf62b5670cca635284d
blob + fcefa1bfce29974926fa660ea36202ef6c5ac19a
--- src/fugu-tty/ui_layout.h
+++ src/fugu-tty/ui_layout.h
size_t cap;
};
+/* Rows available to the picker without touching the persistent status row. */
+struct ui_picker_layout {
+ int top;
+ int height;
+ int model_rows;
+};
+
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);
void ui_layout_plain(struct ui_layout *, const void *, size_t, int,
- int, const char *, const char *, int);
+ int, const char *, const char *, int, int);
int ui_layout_visible_fill(const struct ui_line *, int, int);
+void ui_layout_picker(struct ui_picker_layout *, int, size_t);
#endif /* UI_LAYOUT_H */