• UNICODE @-codes print immediately instead of returning their expansion

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Fri Jul 24 19:29:27 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1198

    The UNICODE-emitting @-codes in `src/sbbs3/atcodes.cpp` write their expansion straight to the terminal from inside `sbbs_t::atcode()` and return `nulstr` instead of returning the expanded string to the caller. Two consequences follow: **format modifiers don't work on them**, and **every caller that expands an @-code for a non-printing reason gets an empty string plus stray output on the user's terminal.**

    ## Affected @-codes

    All of these call `outcp()` (or `wide()`) and `return nulstr;`:

    | @-code | atcodes.cpp |
    |---|---|
    | `@U+XXXX@` (and the `:str`, `,hex`, `!hex` fallback forms) | 506-526 |
    | `@CHECKMARK@` | 528 |
    | `@ELLIPSIS@` | 533 |
    | `@COPY@` | 537 |
    | `@SOUNDCOPY@` | 541 |
    | `@REGISTERED@` | 545 |
    | `@TRADEMARK@` | 549 |
    | `@DEGREE_C@` | 553 |
    | `@DEGREE_F@` | 557 |
    | `@WIDE:text@` | 562 |

    This is *not* about the deliberately state-changing codes (`@SHOW:`, `@HOT:`, `@RAINBOW:`, `@MNE:`, `@CLEAR_HOT@`, …) — those are actions by design and correctly return nothing.

    ## 1. Format modifiers are inert (and actively wrong)

    `show_atcode()` parses the modifiers into `atcode_format fmt` *before* calling `atcode()` (atcodes.cpp:179-181), then applies alignment/width/truncation to the returned string (atcodes.cpp:196-236). Since these codes have already emitted their glyph by the time `atcode()` returns, the modifiers are applied to an empty string.

    So `@CHECKMARK-R10@` emits the check mark, then right-aligns `""` in a 10-column field — i.e. the glyph followed by 10 spaces, rather than 9 spaces followed by the glyph. Same story for `-L`, `-C`, `-Z`, `-U`, `-T`, `->` and the `|` forms.

    The screen-width clamp at atcodes.cpp:202-209 (which keeps an @-code from running past `cols`) also never sees these, so they can overrun the last column.

    Note `show_atcode()` is *already* equipped for this: it handles `P_UTF8` for display-width accounting at atcodes.cpp:210-215, which today only the `MSG_*` codes set.

    ## 2. Non-printing expansion paths get "" and unwanted output

    `formatted_atcode()` (atcodes.cpp:341) and `expand_atcodes()` (atcodes.cpp:2635) exist to expand @-codes **into a buffer**. Both reach the same `atcode()`, so a UNICODE @-code prints to the current node's terminal as
    a side effect and contributes nothing to the buffer. Affected callers:

    - `bbs.atcode()` — js_bbs.cpp:1775
    - `bbs.expand_atcodes()` — js_bbs.cpp:1824
    - `str.cpp:166,178` — expansion of `text.dat` strings into `format_text_buf`
    for later formatting
    - `getkey.cpp:200` — prompt string expansion
    - `putnode.cpp:47` — the node action string written to `node.ext`. The glyph
    goes to *this* node's terminal; other users' who's-online display gets
    nothing.
    - `postmsg.cpp:738` — the `notify()` subject line. The glyph is printed to the
    terminal and dropped from the subject.
    - `putmsg.cpp:476` — `term->center(expand_atcodes(...))`, so the centering math
    runs on the empty expansion.

    It also breaks JS modules that expand @-codes into an off-screen buffer, e.g. `exec/load/graphic.js` and `exec/load/frame.js` both do
    `return bbs.atcode(code)` inside a substitution callback — a UNICODE @-code there writes directly to the terminal, corrupting the frame, and substitutes nothing.

    ## Suggested direction

    Have these codes *return* their expansion instead of printing it:

    - if `term->charset() == CHARSET_UTF8`: `utf8_putc()` the codepoint into `str`,
    set `*pmode |= P_UTF8`, return `str`
    - otherwise: return the CP437/ASCII fallback

    `show_atcode()`'s existing `P_UTF8` handling then makes alignment and width correct for free.

    Open questions for whoever takes this:

    - `formatted_atcode()` and `expand_atcodes()` call `atcode()` with the default
    `pmode = NULL`, so they can't currently learn that the result is UTF-8.
    They'd want a `pmode` out-param — at minimum for `putmsg.cpp:476`, whose
    `center()` needs a display width, not `strlen()`.
    - `@WIDE:` is the harder case, but should be fixed along with the rest: it
    expands one character to two and can emit fullwidth codepoints, so a long
    argument can overflow the 128-byte `str` buffer that `show_atcode()` passes
    in. That buffer likely needs to grow, or `@WIDE:` needs a defined truncation
    point.
    - Minor behavior change: today a UNICODE @-code inside a
    `bbs.expand_atcodes()` result prints at expansion time. Anything (probably
    accidentally) relying on that would change.

    Related but out of scope: `@TYPE:` / `@INCLUDE:` (atcodes.cpp:1855,1860) call `printfile()` and have the same expand-into-a-buffer hazard, though for those the printing is the documented purpose. See also #1191.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)