Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

138 changes: 138 additions & 0 deletions docs/rtl/README.ar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# دعم الكتابة من اليمين لليسار (RTL / BiDi) في واجهة OpenCode النصية

بقى النص العربي والفارسي والأردي والعبري يظهر صح في واجهة OpenCode النصية —
في **مربع الكتابة (البرومبت)** وفي **ردود المساعد** — عن طريق تنفيذ خوارزمية
يونيكود للاتجاهين (UAX #9) في طبقة الرسم داخل التطبيق نفسه، من غير أي اعتماد
على دعم الـ terminal، ومن غير أي إعادة كتابة destructive للنص.

> English: [README.md](./README.md)

## إيه اللي بيشتغل

- **العربي لوحده** يقرأ طبيعي من اليمين لليسار ويتحاذى لليمين.
- **عربي + إنجليزي** بيفضل الإنجليزي (`npm`، `API`، أسماء المتغيرات) في مكانه
الطبيعي حسب ترتيب يونيكود ثنائي الاتجاه.
- **المقاطع اللاتينية بتفضل LTR**: أكواد inline، أسماء ملفات، مسارات، روابط،
أوامر، أسماء حزم، وأرقام — كلها بتتعزل بـ LRI/PDI.
- **الكود دايمًا LTR**: بلوكات الكود والجداول والـ diffs بتستخدم الراسم
الأصلي ومابيتغيرش ترتيبها أبدًا.
- **الالتفاف (wrapping)** محسوب بالـ grapheme وبعرض الخلية؛ أسطر الالتفاف
بتفضل متحاذية يمين وبالترتيب البصري الصح.
- **مؤشر الكتابة واعي بالاتجاه**: الأسهم بتتحرك بصريًا؛ الكتابة واللصق
والتراجع والقيمة المخزنة تفضل يونيكود منطقي.
- **النسخ واللصق سليم**: التحديد بيرجّع النص المنطقي الأصلي، مش ترتيب بصري
ولا حروف تحكم مخفية.

## الخطوط

التطبيق ميقدرش يختار خطوط — الـ terminal هو اللي بيختار. سيب الخط الـ
monospaced زي ما هو واتأكد إن في خط عربي متاح كـ fallback في إعدادات
الـ terminal (مثلًا `"face": "Cascadia Mono, Cairo, Segoe UI"` في
Windows Terminal)، وبعدين اقفل التيرمنال وافتحه تاني.

## التثبيت (ويندوز، من نفس المستودع)


```powershell
# 1) تثبيت اعتماديات الـ workspace
bun install

# 2) بناء ملف تنفيذي لمنصتك الحالية
bun run --cwd packages/opencode script/build.ts --single --skip-install --skip-embed-web-ui

# 3) تشغيله
.\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
```

لاستبدال نسخة npm المثبتة (مع الاحتفاظ بنسخة احتياطية):

```powershell
$new = ".\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe"
$stable = "$env:APPDATA\npm\node_modules\opencode-ai\bin\opencode.exe"
Copy-Item $stable "$stable.original-backup" -Force
Copy-Item $new $stable -Force
```

## المعمارية

الواجهة مبنية على `@opentui/solid` فوق `@opentui/core@0.4.5`. تخطيط النص
والالتفاف والتشكيل ورسم الخلايا كلها بتحصل داخل **مكتبة Zig أصلية**
(`bufferDrawTextBufferView` / `drawEditorView`) مفيها أي دعم BiDi ومفيش بديل
JS. الحزمة المنشورة بتيجي JavaScript مبنية ومضغوطة، فالتعديل ده **مش** بيعدل
OpenTUI نفسه؛ هو بيعمل subclass للكلاسات العامة بتاعته وبيبدّل بس نقطة الرسم
ونقطة رسم المؤشر.

| الجزء | الملف |
| --- | --- |
| محرك UAX #9 ونموذج الـ grapheme والالتفاف والعزل وخرايط المؤشر | `packages/tui/src/util/bidi.ts` |
| مربع الكتابة (`BidiTextareaRenderable`) | `packages/tui/src/component/bidi-textarea.ts` |
| ماركداون ردود المساعد (hook اسمه `renderNode`) | `packages/tui/src/component/bidi-markdown.ts` |
| صدى رسالة المستخدم (`BidiTextRenderable`) | `packages/tui/src/component/bidi-text.ts` |
| تسجيل العناصر (`extend`) | `packages/tui/src/component/bidi-elements.ts` |

الربط:

- `packages/tui/src/component/prompt/index.tsx` — `<textarea>` بتتحول لـ `<bidi_textarea>`
- `packages/tui/src/routes/session/index.tsx` — `<markdown renderNode={bidiMarkdownRenderNode}>`
ورسالة المستخدم `<text>` بتتحول لـ `<bidi_text>`

### اكتشاف الاتجاه

لكل فقرة، بنعمل فحص سريع لأي كتابة RTL قوية (عبري، عربي، سرياني، ثانا، نكو،
مندائي، وأشكال العرض). لو موجودة، `bidi-js` بيحسب مستويات التضمين واتجاه
الفقرة حسب UAX #9 (أول حرف قوي، مع احترام العوازل). الفقرة مابتتصنفش RTL لمجرد
إن فيها اسم إنجليزي؛ والمحتوى الإنجليزي البحت بيمشي على المسار الأصلي حرفيًا.

### العزل بدل إعادة الكتابة

بنجمّع محتوى LTR (روابط، مسارات، أسماء بنقط أو شرطات، أرقام، أكواد inline)
جوّه LRI/PDI. العوازل دي موجودة بس في تدفّق تخطيط داخلي — مابتترسمش ومابتتخزنش،
فالنسخ واللصق بيرجّعوا النص المنطقي النظيف.

### الالتفاف

تقسيم الـ graphemes بـ `Intl.Segmenter`، وعرض الخلية بـ `Bun.stringWidth`
(السطر الجديد بيتحسب عرض 1 زي اصطلاح الـ textarea). النص المنطقي الملتف بيتزامن
مع الـ buffer الأصلي عشان القياس والتحديد والنسخ، وبعدين كل سطر بصري بيتعاد
ترتيبه بـ UAX #9 (L2)؛ أسطر RTL بتتحاذى يمين وأسطر LTR بتتحاذى شمال.

### خرايط المؤشر

كل سطر بيخزّن `boundaryCols` (حدود منطقية ← عمود بصري) و`logicalWidths`
(حدود منطقية ← عرض منطقي). المؤشر المنطقي ← حد ← مكان بصري؛ والحركة بتمشي
على الأعمدة البصرية عبر حدود الأسطر؛ والتحديد بـ Shift بيستخدم آلية التحديد
المنطقية الأصلية.

## الاعتمادية

[`bidi-js@1.1.0`](https://www.npmjs.com/package/bidi-js) — رخصة MIT،
JavaScript خالص، بدون اعتماديات وقت التشغيل، ومُختبر ضد مجموعة اختبارات
يونيكود الرسمية للاتجاهين. بيوفّر بدائيات UAX #9 (حساب المستويات وإعادة
الترتيب والانعكاس)؛ وكل سياسات الرسم موجودة في المستودع ده.

## الاختبارات

```bash
bun test --cwd packages/tui ./test/util/bidi.test.ts
bun test --cwd packages/tui ./test/component/bidi-render.test.tsx
bun test --cwd packages/tui ./test/component/bidi-mixed.test.tsx
bun test --cwd packages/tui ./test/bidi-e2e.test.tsx
bun test --cwd packages/tui ./test/bidi-dialogs-e2e.test.tsx
bun run --cwd packages/tui typecheck
```

## حدود معروفة

- **تشكيل** الحروف العربية متروك للـ terminal والخط؛ التعديل ده بيتحكم في
الترتيب والاتجاه والمحاذاة، وهوه اللي الراسم الخلوي يقدر يتحكم فيه.
- إخفاء الماركداون (backticks المخفية) ممكن يفقد ألوان بعض المقاطع لفترة
بسيطة وقت البث؛ لكن الترتيب دايمًا صح.
- جداول الماركداون بتفضل بالراسم الأصلي: العربي جوه الخلايا بيظهر بالترتيب
المنطقي على الشمال. الجداول سطح بيانات، وإعادة تدفقها حسب الاتجاه هتكسر
محاذاة الأعمدة والنسخ/اللصق.
- سطور الـ diff بتحافظ على بنية الكود؛ والنص العربي في السطور المضافة/المحذوفة
بيتبع اتجاه الفقرة وعلامات +/- وأرقام السطور ثابتة.
- علامات القوائم (`-`، `1.`) بتفضل على الحرف الشمال؛ ومحتوى العنصر بيتبع
اتجاه الفقرة.
- `Home`/`End` والقفز بين الكلمات لسه بمنطق الإزاحة المنطقية (سلوك أصلي).
- الضغط بالماوس في البرومبت لسه بالسلوك الأصلي؛ حركة الكيبورد واعية بالكامل.
154 changes: 154 additions & 0 deletions docs/rtl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Native BiDi / RTL support in the OpenCode TUI

Arabic, Persian, Urdu and Hebrew text now renders correctly in the native TUI —
in the **prompt input** and in **assistant output** — implementing the Unicode
Bidirectional Algorithm (UAX #9) at the application rendering layer, with no
terminal-side BiDi requirement and no destructive text rewriting.

> العربية: [README.ar.md](./README.ar.md)
## What works

- **Arabic-only text** reads naturally right-to-left and is right-aligned.
- **Mixed Arabic + English** keeps embedded English (`npm`, `API`, identifiers)
in natural bidi flow.
- **LTR islands stay LTR**: code spans, filenames, paths, URLs, commands,
package names and numbers are directionally isolated with LRI/PDI.
- **Code always stays LTR**: fenced code blocks, tables and diffs keep the stock
renderer and are never reordered.
- **Wrapping** is grapheme- and width-aware; wrapped RTL continuation lines stay
right-aligned and in the correct visual order.
- **The prompt caret is bidi-aware**: arrow keys move visually; typing, paste,
undo and the stored value remain logical Unicode.
- **Copy/paste is untouched**: selection returns the original logical text,
never visual-order or control characters.

## Fonts

The TUI cannot select fonts — the terminal does. Keep your monospaced font and
make sure an Arabic-capable font is available as a fallback in your terminal
settings (for example `"face": "Cascadia Mono, Cairo, Segoe UI"` in Windows
Terminal), then restart the terminal.

## Install (Windows, from this repository)

```powershell
# 1) install workspace dependencies
bun install
# 2) build a single-platform native binary
bun run --cwd packages/opencode script/build.ts --single --skip-install --skip-embed-web-ui
# 3) run it
.\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
```

To replace an existing npm install (keep a backup of the original binary):

```powershell
$new = ".\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe"
$stable = "$env:APPDATA\npm\node_modules\opencode-ai\bin\opencode.exe"
Copy-Item $stable "$stable.original-backup" -Force
Copy-Item $new $stable -Force
```

## Architecture

The TUI is `@opentui/solid` over `@opentui/core@0.4.5`. Text layout, wrapping,
shaping and cell painting in OpenTUI all happen in a **native Zig library**
(`bufferDrawTextBufferView` / `drawEditorView`) that has no BiDi support and no
JS fallback. The published package ships bundled JS, so this change does **not**
patch OpenTUI; it subclasses its public renderables and overrides only the paint
and caret entry points:

| Piece | File |
| --- | --- |
| UAX #9 engine wrapper, grapheme model, wrapping, isolates, cursor maps | `packages/tui/src/util/bidi.ts` |
| Prompt input (`BidiTextareaRenderable extends TextareaRenderable`) | `packages/tui/src/component/bidi-textarea.ts` |
| Assistant markdown (`renderNode` hook for paragraphs/headings) | `packages/tui/src/component/bidi-markdown.ts` |
| User message echo (`BidiTextRenderable extends TextRenderable`) | `packages/tui/src/component/bidi-text.ts` |
| Element registration (`extend({ bidi_text, bidi_textarea })`) | `packages/tui/src/component/bidi-elements.ts` |

Wiring:

- `packages/tui/src/component/prompt/index.tsx``<textarea>``<bidi_textarea>`
- `packages/tui/src/routes/session/index.tsx``<markdown renderNode={bidiMarkdownRenderNode}>`
and user echo `<text>``<bidi_text>`

### Direction detection

Per paragraph, a strong-RTL script probe runs first (Hebrew, Arabic, Syriac,
Thaana, NKo, Mandaic, presentation forms). If found, `bidi-js` resolves embedding
levels and the paragraph base direction via UAX #9 P2/P3 (first strong
character, isolates honored). A paragraph is never classified RTL just because
it contains an English identifier; English-only content takes the stock path
bit-for-bit.

### Isolation instead of rewriting

`bidi-js` is wrapped in LRI/PDI for LTR islands (URLs, `a/b.c` paths, dotted and
dashed identifiers, numbers, markdown code spans). Isolates exist only in a
layout-only augmented stream — they are never painted and never stored, so
selection/copy return pristine logical text.

### Wrapping

Grapheme segmentation via `Intl.Segmenter`; cell widths via `Bun.stringWidth`
(newlines count as one width unit, matching the textarea offset convention).
Wrapped **logical** text is synced into the native text buffer so measurement,
selection and copy keep working, then each visual line is reordered
independently with UAX #9 L2 flip segments; RTL lines align to the right edge,
LTR lines to the left.

### Caret mapping

Each line stores `boundaryCols` (logical boundary → visual column) and
`logicalWidths` (logical boundary → logical width). Logical cursor → boundary →
visual caret; caret motion walks visual columns monotonically across line edges;
shift-selection reuses the native logical selection machinery.

## Dependency

[`bidi-js@1.1.0`](https://www.npmjs.com/package/bidi-js) — MIT, pure JS, no
runtime dependencies, verified against the Unicode bidi conformance suite. It
provides the UAX #9 primitives (`getEmbeddingLevels`, `getReorderSegments`,
mirroring); all rendering policy lives in this repository.

## Tests

```bash
bun test --cwd packages/tui ./test/util/bidi.test.ts
bun test --cwd packages/tui ./test/component/bidi-render.test.tsx
bun test --cwd packages/tui ./test/component/bidi-mixed.test.tsx
bun test --cwd packages/tui ./test/bidi-e2e.test.tsx
bun test --cwd packages/tui ./test/bidi-dialogs-e2e.test.tsx
bun run --cwd packages/tui typecheck
```

- `test/util/bidi.test.ts` — engine: detection, ordering, isolates, wrapping,
cursor round-trips.
- `test/component/bidi-render.test.tsx` — real OpenTUI test renderer: RTL
painting, right alignment, code LTR, English-identical-to-stock, caret, copy.
- `test/component/bidi-mixed.test.tsx` — mixed-direction paragraphs: code
spans, commands, dotted identifiers, spaced Windows paths, URLs, versions,
key:line references, bold markers, lists, links, tables of punctuation,
tashkeel preservation, wrapping around LTR tokens.
- `test/bidi-e2e.test.tsx` — boots the real app, types Arabic with real key
presses, streams a real assistant message.

## Known limitations

- Arabic glyph **shaping** is left to the terminal/font; this change controls
order, direction and alignment, which is what a cell-grid renderer can own.
- Markdown `conceal` (hidden backticks) may drop per-span colors in a brief
streaming window; order always stays correct.
- Markdown tables keep the stock grid renderer: Arabic inside table cells
renders in logical order, left-aligned. Tables are a data surface; reflowing
them per direction would break column alignment and copy/paste.
- Diff hunks keep code structure; Arabic prose inside added/removed lines
follows paragraph direction while signs and line numbers stay put.
- List markers (`-`, `1.`) stay on the left edge; item content follows
paragraph direction.
- `Home`/`End`/word-jump keep logical-offset semantics (native behavior).
- Mouse click positioning in the prompt keeps native behavior; keyboard motion
is fully bidi-aware.
55 changes: 55 additions & 0 deletions docs/rtl/install-windows.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Builds the RTL/BiDi dev build and installs it over the npm-installed
# `opencode` (stable) and `opencode2` (beta @opencode-ai/cli) binaries on
# Windows. Original binaries are kept next to the new ones as
# `*.original-backup` so restore-windows.ps1 can roll back.
#
# Usage (from the repository root):
# powershell -ExecutionPolicy Bypass -File docs/rtl/install-windows.ps1

$ErrorActionPreference = "Stop"

$repo = Resolve-Path (Join-Path $PSScriptRoot "..\..")
$exe = Join-Path $repo "packages\opencode\dist\opencode-windows-x64\bin\opencode.exe"

Write-Host "Building single-platform native binary..."
Push-Location $repo
try {
bun run --cwd packages/opencode script/build.ts --single --skip-install --skip-embed-web-ui
} finally {
Pop-Location
}

if (-not (Test-Path $exe)) {
throw "Build did not produce $exe"
}

$targets = @(
(Join-Path $env:APPDATA "npm\node_modules\opencode-ai\bin\opencode.exe"),
(Join-Path $env:APPDATA "npm\node_modules\@opencode-ai\cli\bin\opencode2.exe")
)

foreach ($target in $targets) {
if (-not (Test-Path $target)) {
Write-Host "Skipping (not installed): $target"
continue
}
$backup = "$target.original-backup"
if (-not (Test-Path $backup)) {
Copy-Item $target $backup -Force
Write-Host "Backed up: $backup"
}
try {
Copy-Item $exe $target -Force
Write-Host "Installed: $target"
} catch {
# A running instance locks the executable. Windows allows renaming a
# running image, so move it aside first, then place the new build.
Move-Item -LiteralPath $target -Destination $backup -Force
Copy-Item $exe $target -Force
Write-Host "Installed (renamed running binary): $target"
}
}

Write-Host ""
Write-Host "Done. Restart any running OpenCode window to pick up the new build."
Write-Host 'Verify with: opencode --version'
31 changes: 31 additions & 0 deletions docs/rtl/restore-windows.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Restores the npm-installed `opencode` (stable) and `opencode2` (beta)
# binaries that install-windows.ps1 replaced.
#
# Usage (from the repository root):
# powershell -ExecutionPolicy Bypass -File docs/rtl/restore-windows.ps1

$ErrorActionPreference = "Stop"

$targets = @(
(Join-Path $env:APPDATA "npm\node_modules\opencode-ai\bin\opencode.exe"),
(Join-Path $env:APPDATA "npm\node_modules\@opencode-ai\cli\bin\opencode2.exe")
)

foreach ($target in $targets) {
$backup = "$target.original-backup"
if (-not (Test-Path $backup)) {
Write-Host "No backup found for: $target"
continue
}
try {
Copy-Item $backup $target -Force
Write-Host "Restored: $target"
} catch {
Move-Item -LiteralPath $target -Destination "$target.rtl-build" -Force
Copy-Item $backup $target -Force
Write-Host "Restored (renamed running binary): $target"
}
}

Write-Host ""
Write-Host "Done. Restart any running OpenCode window to pick up the original build."
Loading
Loading