Skip to content

feat: draw right-to-left terminal text in reading order (terminal_bidi) - #838

Open
1Goltsman1 wants to merge 2 commits into
herdrdev:masterfrom
1Goltsman1:feat/terminal-bidi
Open

1Goltsman1 wants to merge 2 commits into
herdrdev:masterfrom
1Goltsman1:feat/terminal-bidi

Conversation

@1Goltsman1

@1Goltsman1 1Goltsman1 commented Nov 10, 2026 •

Copy link
Copy Markdown

Adds an opt-in terminal_bidi setting (default off). With it on, terminal rows that hold right-to-left characters (Hebrew, Arabic, ...) are drawn with the Unicode Bidirectional Algorithm, each row a left-to-right paragraph (the terminal-wg "implicit" mode, as in iTerm2 and mlterm). Today such rows are drawn strictly left to right, so Hebrew typed into an agent's prompt, and Hebrew output, read reversed. Only the drawing changes: the grid, copy, search, links, hc read and selection stay in logical order. A mod can't change how panes draw, so this is app code.

Rebased onto current master (5216e6f).

How

  • src/bidi.rs: a quick per-row scan for strong RTL characters (rows without them are untouched); unicode-bidi (already a transitive dependency) resolves levels; logical↔visual column maps; a wide character keeps head and tail adjacent.
  • Terminal rows are laid out in columns, so a row is cut into segments that are reordered on their own: box-drawing/block characters and runs of two or more blank cells keep their columns. │ שם │ גודל │ no longer swaps its columns (segmenting idea from feat: draw right-to-left text right to left with joined letters #945).
  • In file paths and URLs (Ghostty's link pattern, new links::spans) / and \ count as left to right, so /Users/dan/פרויקטים/תיקייה/src keeps its folders in order while each Hebrew name still reads right to left. A slash in prose (כן/לא) still reads right to left.
  • row_ops places backgrounds, text runs, sprites, icons and row_decorations at visual columns. Text runs keep logical text, so shaping (Arabic joining, mirrored brackets) still happens in the platform shaper, and runs break where visual adjacency breaks. Search and link highlights, the cursor and the IME box follow their cells; clicks map visual→logical.
  • HERDR: patch to ShapedLine::with_cell_positions: each cluster moves by one shift taken from its lowest-index glyph, so niqqud that CoreText emits before its base stays on its letter (fix from fix(macos): restore keyboard suggestions and add opt-in Hebrew BiDi #916). Left-to-right text gets the same result as before.
  • terminal_bidi in settings.json, documented in defaults/AGENTS.md; the painted-row cache key includes it, so toggling applies live.

Tests (cargo test --locked on macOS: 509 passed, 0 failed, 2 ignored)

  • bidi::tests: pure Hebrew, mixed Hebrew/English, numbers (prices, dates, versions, phone numbers), commands and prompts, brackets, wide characters and emoji, niqqud cells, table borders, aligned columns, paths and URLs, cursor and click mapping. The border, column and path tests fail on the previous version of this PR.
  • Real escape sequences through libghostty: SGR color changes mid-word and an OSC 8 link never reach the reordering; colors follow their letters.
  • 5,000 seeded random rows (Hebrew, Latin, digits, punctuation, borders, blanks, wide characters) checked against invariants: the mapping is a permutation, wide tails stay adjacent, borders keep their columns, Latin pairs read left to right, Hebrew pairs right to left.
  • A font-independent regression for the niqqud anchoring (fails without the patch), and render tests for glyph columns and backgrounds.
  • Also checked with the macOS text system offscreen (render_to_image, not part of the PR): each sample row drawn off and on; colors follow their letters, borders stay aligned, niqqud sits on its letters, and a path's folders were measured to keep their columns.

Known limits (fine for an opt-in first step, happy to iterate)

  • Fixed LTR paragraph direction: a row that starts with a number keeps it on the left, and a Hebrew sentence with a Latin tail keeps LTR word order.
  • The cursor sits on its logical cell, so while typing Hebrew it stays to the right of the word. A follow-up can draw the caret at the insertion point; agents that hide the terminal cursor and draw their own cell aren't helped by that.
  • Drag selection across mixed runs selects logical ranges (as iTerm2 does).
  • No BDSM (CSI 8 l) opt-out yet for programs that do their own bidi; planned as a follow-up, watched app-side in Vt::write like win32.watch, with no Ghostty patch.

Part of #804 and #847 (RTL/BiDi). Builds on ideas from #916 and #945. Math formula rendering is out of scope.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant