Skip to content

Unicode & CJK Support ​

RTXUI measures text in grapheme clusters and computes display widths per cluster, so layout works with any script the terminal can render.

Wide Characters ​

Wide characters (such as Chinese, Japanese, and Korean ideographs) occupy two terminal cell columns. RTXUI accounts for this when measuring text, so wide characters align correctly in flexbox and grid layouts.

Control Characters ​

Text is displayed, never executed. An application shows text it did not write -- a filename, a log line, a response from the network -- and a byte like ESC in there is a command to the terminal rather than a character: it would repaint, move the cursor, or recolour the interface, and everything after it on the line would end up somewhere other than where the layout put it.

Every control character except newline and tab is therefore replaced with U+FFFD (the replacement character) before measurement, so it occupies exactly the one cell it is counted as. Newline and tab survive because layout gives them a meaning of its own -- a hard break and a tab stop. A carriage return becomes a line break, as CSS treats it.

Malformed Text ​

Byte sequences that are not valid UTF-8 are replaced the same way, one U+FFFD per bad byte. This is not pedantry about encodings: the engine and the terminal have to agree on how many cells a sequence occupies, and they can only agree about sequences that can actually be decoded.

The sharpest case is a truncated one. A lead byte announces how many bytes follow it, so E4 BD at the end of a string claims a third byte that is not there and takes whatever comes next instead -- the character after it is absorbed and simply disappears from the display. Replacing per byte rather than per sequence is what keeps the following character intact.

Valid text is never touched, including the parts of Unicode that are easy to mistake for damage: wide characters, combining marks, zero-width characters and four-byte code points all pass through as written.

Zero-Width Characters ​

Format characters occupy no cell, and are measured that way: the zero-width space and non-joiner, the bidi marks, embeddings, overrides and isolates, the word joiner and invisible operators, and the byte-order mark. These arrive in ordinary text -- a zero-width space pasted from a web page, a bidi mark inside a filename, a byte-order mark at the head of a file -- and counting one as a cell would put everything after it on that line a column to the left of where the layout expects it.

Nothing is required of you to get any of this: it applies to every string a template interpolates, whatever its source.

Glyphs the Terminal Lacks ​

Borders, scrollbars and the marks of the built-in tags (☑, ◉, ▾, …) are drawn with Unicode glyphs. Most terminals draw a glyph their font lacks with another font. The Windows console (conhost) does not: it shows a boxed ?, and its default font, Consolas, lacks the eighth blocks border: tall is made of.

In the Windows console, RTXUI asks the console font which of these glyphs it has, and replaces each one it lacks with the closest one it has, down to ASCII: ▊ becomes ▌, ╭ becomes ┌. A border: tall then looks a little heavier there, instead of broken. Elsewhere, including Windows Terminal, which falls back to other fonts, nothing is replaced. Text you write is never replaced: choose its glyphs for the fonts your users have.

The RTXUI_GLYPHS environment variable overrides this:

ValueEffect
fullReplace nothing: the terminal draws every glyph.
basicReplace every glyph outside WGL4, the repertoire Windows fonts cover. For a terminal RTXUI cannot ask, like one connected over SSH.

Layout Alignment ​

Because widths are computed from display columns rather than byte or code point counts, centering and alignment stay correct when ASCII and wide characters are mixed on the same line.

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Double-width text.
//
// CJK ideographs and emoji occupy two terminal cells. Layout measures text in
// cells, not code points, so alignment holds for mixed-width content.
#include <rtxui/rtxui.hpp>
#include <string_view>

using namespace rtxui;

class CJKDemo : public Component<CJKDemo> {
 public:
  std::string_view view = R"html(
      <div class="card">
        <h1>Double-width text</h1>
        <p>CJK ideographs take two cells each. Layout counts cells, so the columns stay aligned.</p>

        <div class="table">
          <span class="lang">English</span>
          <span>Terminal user interface</span>
          <span class="lang">Chinese</span>
          <span>终端用户界面</span>
          <span class="lang">Japanese</span>
          <span>ターミナルの画面</span>
          <span class="lang">Korean</span>
          <span>터미널 사용자 인터페이스</span>
          <span class="lang">Mixed</span>
          <span>RTXUI 支持中文 and 日本語</span>
        </div>
      </div>

      <style>
        self {
          --border: rgb(48, 54, 61);
          --muted: rgb(139, 148, 158);
          --accent: rgb(88, 166, 255);

          display: flex;
          align-items: center;
          justify-content: center;
          width: 100%;
          color: rgb(230, 237, 243);
        }
        .card {
          border: tall;
          border-color: var(--border);
          background-color: rgb(22, 27, 34);
          padding: 1 3;
          width: 56;
        }
        h1 {
          color: var(--accent);
        }
        p {
          color: var(--muted);
        }
        .table {
          display: grid;
          grid-template-columns: 10 auto;
          border: round;
          border-color: var(--border);
          padding: 0 1;
        }
        .lang {
          color: var(--muted);
        }
      </style>
    )html";
};

int main() {
  auto app = Ref<CJKDemo>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}