open
https://gitlab.synchro.net/main/sbbs/-/issues/1200
`sbbs_t::outcp()` advances the terminal column counter twice for every codepoint it writes to a UTF-8 terminal, so the column counter runs ahead of the actual cursor position.
## The double-count
`outcp()` (`src/sbbs3/con_out.cpp:800`) writes the codepoint with `term_out()` and *then* advances the column itself:
```cpp
int sbbs_t::outcp(enum unicode_codepoint codepoint, const char* cp437_fallback) {
if (term->charset() == CHARSET_UTF8) {
char str[UTF8_MAX_LEN];
int len = utf8_putc(str, sizeof(str), codepoint);
if (len < 1)
return len;
term_out(str, len);
term->inc_column(unicode_width(codepoint, unicode_zerowidth)); // <-- second count
return 0;
}
...
}
```
But `term_out()` → `parse_output()` → `Terminal::utf8_increment()` (`src/sbbs3/terminal.cpp:369-398`) has *already* counted it:
```cpp
if (utf8_remain)
return true;
inc_column(unicode_width(static_cast<enum unicode_codepoint>(codepoint), 0));
```
`utf8_increment()` runs for every UTF-8 terminal — it is gated on
`flags_ & UTF8`, which is exactly the condition `charset() == CHARSET_UTF8` tests, and both `ANSI_Terminal::parse_output()` (`ansi_terminal.cpp:863`) and the base `Terminal::parse_output()` (`terminal.h:408`) call it. (PETSCII terminals can't be UTF-8: `flags_fixup()` clears `UTF8` when `PETSCII` is set.)
`bputs()` with `P_UTF8` reaches `term_out()` by the same route and does *not* add its own `inc_column()`, so it produces the correct count — which makes it a
convenient control.
## Reproducer
[outcp_column_test.js](/uploads/e35955a507cc8b37abd97110a1337af5/outcp_column_test.js)
Requires a terminal session with a UTF-8 charset (it bails out with an explanation otherwise); `jsexec` has no `console` object. It exits 0 when the two paths agree, so it doubles as a regression check.
`console.wide()` is the only remaining JS path to `outcp()`. It emits
U+FF21 FULLWIDTH LATIN CAPITAL LETTER A, display width 2. The control prints the identical three bytes via `console.print("\xEF\xBC\xA1", P_UTF8)` — same bytes on the wire, same glyph on screen, only the column counter differs:
```
outcp() column double-count test
--------------------------------
terminal charset: UTF-8
Both lines below print the same glyph, via different code paths:
A
console.print(bytes, P_UTF8) column += 2 (expected 2) ok
A
console.wide('A') [via outcp] column += 4 (expected 2) *** DOUBLE-COUNTED ***
ABCDEFGHIJ
console.wide('ABCDEFGHIJ') column += 40 (expected 20) *** DOUBLE-COUNTED ***
FAIL: outcp() reports 4 columns where bputs() reports 2
(ratio 2.0; a 10-char run reported 40 columns instead of 20)
```
## Consequences
The column counter feeds right-margin truncation, word-wrap, centering and the auto-pause line counter, so any line containing `outcp()`-emitted text mis-measures — wrapping or truncating early.
A second symptom of the same line: `term_out()` returns early when `CON_ECHO_OFF` is set, but the `inc_column()` call is unconditional, so the column also advances for output that was suppressed.
## Exposure
Since b17b1d3e3e (#1198 — Unicode @-codes now return their expansion rather than printing it), `outcp()` is reachable only via `sbbs_t::wide()`, itself reachable
only from the `|W` (doubled) @-code format modifier and `console.wide()`. Before
that commit every Unicode @-code — `@CHECKMARK@`, `@U+XXXX@`, `@ELLIPSIS@` and
friends — went through it.
## Suggested fix
Delete the `inc_column()` call in `outcp()` and let `utf8_increment()` be the single place that counts columns.
The two sites don't agree on a second point, though: `outcp()` passes `unicode_zerowidth` to `unicode_width()` while `utf8_increment()` hardcodes `0`.
`unicode_zerowidth` is the per-session quirk detected by the ZWNBSP probe at logon (`answer.cpp:668`, set to 1 when the terminal renders zero-width characters as one column). Deleting `outcp()`'s call alone would silently discard that detection, so `utf8_increment()` should pass `sbbs->unicode_zerowidth` instead of `0` (`unicode_zerowidth` is a public `sbbs_t` member and `Terminal` holds an `sbbs` pointer).
Note that the second half of that change affects **all** UTF-8 output including `bputs()`, not just `outcp()`: on a terminal where the probe found
`zerowidth == 1`, lines containing zero-width codepoints would start counting differently than they do today. The reproducer above won't catch that — U+FF21
isn't zero-width.
— *Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.37-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)