رفتن به محتوا

ar-terminal (RTL Terminal)

  • مخزن: https://github.com/alzahrani-khalid/ar-terminal · کامیت خوانده‌شده: fa0690f (v0.3.4، 2026-03-29) · مجوز: ندارد. فایل LICENSE وجود ندارد. package.json:6 و README.md:154 می‌گویند «MIT»، اما بدون متن مجوز، کد را غیرقابل استفادهٔ دوباره در نظر می‌گیریم. · زبان: TypeScript · سکو: افزونهٔ VS Code (macOS/Linux/Windows)، node-pty + xterm.js 6 درون یک webview
  • دسته: رندرکنندهٔ لایهٔ رویی (مسیر اصلی) + پراکسی PTY / تبدیل خروجی (مسیر فرعی)

افزونه‌ای برای VS Code که ترمینال خودش را به‌صورت یک webview در زبانهٔ ویرایشگر باز می‌کند: node-pty پوسته را اجرا می‌کند و یک xterm.js بسته‌بندی‌شده ترمینال را شبیه‌سازی می‌کند. سطرهایی که نقطهٔ کد RTL (یا آیکون‌های Nerd Font) دارند، با یک <div> HTML با موقعیت مطلق پوشانده می‌شوند تا موتور متن Chromium آن سطرها را شکل‌دهی (shaping) و بازچینی کند. مسیر کد دوم، یک پروفایل vscode.Pseudoterminal، جریان PTY را به فرم‌های نمایشی (presentation forms) و به ترتیب دیداری (visual order) بازنویسی می‌کند.

چگونه کار می‌کند (تأییدشده در کد)

Section titled “چگونه کار می‌کند (تأییدشده در کد)”

مسیر A، لایهٔ رویی (overlay) در webview (rtlTerminal.newTerminal، src/extension.ts:55-65، src/webview-terminal.ts):

  • بایت‌های PTY خام به xterm.js می‌روند (webview-terminal.ts:133-136). فقط دنباله‌های OSC عنوان حذف می‌شوند (:119-131). xterm.js متن منطقی را در بافرش نگه می‌دارد و آن را روی بوم (canvas) خودش رسم می‌کند.
  • قلاب‌کردن به xterm.js: این نه یک فورک است، نه رندرکنندهٔ سفارشی، نه API تزئین (decoration) و نه character joiner. یک لایهٔ DOM هم‌سطح است به نام #arabic-overlay با pointer-events:none و z-index:10 (:222-243). در رویدادهای onWriteParsed، onScroll، onResize و onSelectionChange (:627-630)، لایهٔ رویی پس از یک debounce شانزده‌میلی‌ثانیه‌ای از نو ساخته می‌شود (:620-625).
  • updateArabicOverlay (:463-535) سطرهای قابل‌مشاهدهٔ term.buffer.active را پیمایش می‌کند. سطری پوشانده می‌شود که هر یک از خانه‌هایش RTL یا گلیف PUA از Nerd Font باشد (:474-482، بازه‌ها در :363-378). برای هر سطرِ پوشانده‌شده، cell.getChars() را به ترتیب منطقیِ خانه‌ها به هم می‌چسباند و بر اساس رنگ پیش‌زمینه/bold/dim/italic در <span>ها گروه‌بندی می‌کند (:495-520). رنگ پس‌زمینه، زیرخط و inverse کنار گذاشته می‌شوند. div سطر مات است (background در :242)، پس سطر بوم زیر خود را پنهان می‌کند.
  • شکل‌دهی و دوجهتی (bidi) را Chromium انجام می‌دهد، نه کد برنامه. div دارای direction:ltr; unicode-bidi:normal; white-space:pre است (:233-243)، پس Blink الگوریتم UAX #9 را با سطح پاراگراف LTR اجرا می‌کند و با HarfBuzz و هر قلمی که از زنجیرهٔ جایگزینی (fallback) برسد شکل‌دهی می‌کند. فیلدهای ArabicReshaper و BidiEngine در این کلاس (:22-24) استفاده نمی‌شوند. reshapeArabicInStream (:159-176) کد مرده است و هیچ فراخواننده‌ای ندارد.
  • از خانه‌ها به گلیف‌ها: هیچ نگاشتی وجود ندارد. لایهٔ رویی از چیدمان متناسب (proportional) مرورگر استفاده می‌کند که از x=0 سطر شروع می‌شود (left:0). فقط top/height سطر از term._core._renderService.dimensions خصوصیِ xterm گرفته می‌شود (:420-432). متن عربیِ شکل‌دهی‌شده باریک‌تر یا پهن‌تر از N خانهٔ خودش است، پس هر چه پس از آن در سطر بیاید از شبکهٔ xterm فاصله می‌گیرد.
  • مکان‌نما (cursor): یک میلهٔ چشمک‌زن ساختگی که در buf.cursorX * cellW قرار می‌گیرد (:522-529). این ستونِ خانهٔ منطقی است که با گلیف‌های بازچینی و بازچیده‌شده روی لایهٔ رویی جور درنمی‌آید.
  • انتخاب: همچنان انتخاب خودِ xterm.js روی شبکهٔ بوم پنهان است (ستون‌های منطقی). لایهٔ رویی فقط با شکستن spanها در افست‌های نویسه‌ایِ منطقی که برابر ستون‌های انتخاب‌اند، برجسته‌سازی را دوباره رسم می‌کند (:538-618). بنابراین کشیدن ماوس بر اساس خانه‌های منطقیِ نامرئی انتخاب می‌کند، نه بر اساس آنچه کاربر می‌بیند.
  • کپی: یک شنوندهٔ copy (:756-806) متن را از spanهای لایهٔ رویی می‌سازد که نویسه‌های بافر را به ترتیب منطقی در خود دارند، و بر اساس ستون substring(startX, endX) را برمی‌دارد. نتیجه ترتیب منطقی با نقاط کد اصلی است (بدون فرم‌های نمایشی). احتمال جابه‌جایی اندیس‌ها وجود دارد، چون خانه‌های خالی یا پهن رد می‌شوند (:499).
  • در این مسیر هیچ بررسی‌ای برای صفحهٔ جایگزین (alternate screen) وجود ندارد، پس سطرهای عربیِ vim/htop هم پوشانده می‌شوند. هیچ addon پهنای Unicode 11 بارگذاری نمی‌شود. تنظیمات rtlTerminal.mode و reshapeInput اینجا هرگز خوانده نمی‌شوند.

مسیر B، پروفایل Pseudoterminal (پروفایل ترمینال «RTL Terminal»، extension.ts:30-46, 90-150):

  • RtlPipeline.process (src/rtl-pipeline.ts:18-83) ابتدا کدهای ANSI را حذف می‌کند، سپس برای هر سطری که RTL دارد: (۱) ArabicReshaper حروف را با Presentation Forms-B جایگزین می‌کند (arabic-reshaper.ts:27-139، جدول در arabic-data.ts)، (۲) BidiEngine با استفاده از bidi-js 1.0.3 (سطوح UAX #9 + قرینه‌سازی) runها را به‌طور فیزیکی وارونه و به ترتیب دیداری درمی‌آورد (bidi-engine.ts:15-68)، و سپس (۳) کدهای ANSI را در موقعیت‌های پیش از بازچینی سر جایشان برمی‌گرداند (rtl-pipeline.ts:82). نگاشت‌های اندیس محاسبه می‌شوند اما دور ریخته می‌شوند.
  • جدول شکل‌دهی فقط U+0621–U+064A به‌علاوهٔ لام‌الف را پوشش می‌دهد (arabic-data.ts). حروف فارسیِ پ چ ژ گ ک ی وجود ندارند، پس به‌صورت حروف پایهٔ نامتصل می‌مانند و اتصال حروف همسایه را هم خراب می‌کنند. نیم‌فاصله (ZWNJ) پردازش نمی‌شود.
  • ورودیِ تایپ‌شده‌ای که بازتاب (echo) می‌شود، نویسه به نویسهٔ عربی گرفته می‌شود و با \b×N و بافر بازشکل‌دهی‌شده دوباره رسم می‌شود (rtl-pipeline.ts:95-139)، بدون هیچ پردازش دوجهتی.
  • صفحهٔ جایگزین (?1049h/?47h) را به حالت عبور مستقیم (pass-through) می‌برد (:20-29).
لایه وضعیت شاهد
۱ رمزگذاری و یکسان‌سازی ❌ هیچ یکسان‌سازی‌ای نیست. مسیر B متن را به فرم‌های نمایشی (نویسه‌های سازگاری) تبدیل می‌کند.
۲ ورودی ◐ مسیر B ترفندِ بازتاب را بازشکل‌دهی می‌کند (rtl-pipeline.ts:110-139). هیچ منطقی برای IME یا نیم‌فاصله وجود ندارد. میان‌برهای Cmd/Alt در Mac به دنباله‌های readline نگاشت می‌شوند (keybindings در package.json).
۳ پهنا ❌ گلیف‌های لایهٔ رویی با خانه‌ها هم‌تراز نیستند (چیدمان متناسب). مسیر B برای هر فرم نمایشی یک خانه فرض می‌کند.
۴ شکل‌دهی ◐ مسیر A: شکل‌دهی واقعی با HarfBuzz از طریق Chromium. مسیر B: فرم‌های نمایشی با جدول جست‌وجو، فقط عربی.
۵ دوجهتی ◐ مسیر A: UAX #9 در Blink برای هر سطر، با پایهٔ LTR اجباری و محدود به همان سطر. مسیر B: UAX #9 با bidi-js، اما ترتیب دیداری را در خود جریان ثابت می‌کند.
۶ مکان‌نما، انتخاب و کپی ◐ در مسیر A کپی منطقی است (:756-806)، اما آزمون برخوردِ انتخاب و مکان‌نما از ستون‌های منطقی روی یک رندر دیداری استفاده می‌کنند. مسیر B فرم‌های نمایشیِ وارونه‌شده را کپی می‌کند.
۷ قلم‌ها ◐ زیرمجموعه‌ای از نمادهای Nerd Font را همراه دارد (:203-210). عربی از جایگزینی قلم مرورگر استفاده می‌کند و قلم عربیِ تک‌فاصله (monospace) ندارد.
۸ مالتی‌پلکسرها ❌ پرداخته نشده. بازترسیمِ tmux/Zellij صرفاً سطرهای بیشتری برای پوشاندن است.
۹ برنامه‌ها و چارچوب‌های TUI ◐ در تبلیغاتش Claude Code را هدف گرفته است. لایهٔ رویی برای برنامه‌های تمام‌صفحه غیرفعال نمی‌شود (مسیر A).
  • «vim / htop / less — TUI apps work normally (overlay auto-disables)» (README.md:76). در لایهٔ رویی webview هیچ بررسی‌ای برای صفحهٔ جایگزین وجود ندارد. فقط مسیر B چنین بررسی‌ای دارد.
  • «Auto-detection / mode auto/on/off» (README.md:44, 105-108). حالت (mode) فقط در مسیر B بررسی می‌شود. فرمان پیش‌فرض (مسیر A) آن را نادیده می‌گیرد.
  • «Full support for 16, 256, and RGB colors» (README.md:46). لایهٔ رویی فقط رنگ پیش‌زمینه را نگه می‌دارد و پس‌زمینه، inverse و زیرخط را کنار می‌گذارد (:501-518).
  • پشتیبانی از «Persian, Urdu» (README.md:43). در مسیر B، حروف فارسی و اردوِ بیرون از بازهٔ 0621–064A شکل‌دهی نمی‌شوند. مسیر A به مرورگر وابسته است.
  • README اعتراف می‌کند که «Text selection on Arabic lines copies from xterm.js underneath» (README.md:146). اما کد در عمل کپی را با متن لایهٔ رویی جایگزین می‌کند، پس README قدیمی شده است.
  • سند طراحی (docs/superpowers/specs/2026-03-25-…md:28) رویکرد Pseudoterminal همراه با بازچینی را توصیف می‌کند. اما پیش‌فرضِ منتشرشده به لایهٔ رویی تغییر کرده است.

آنچه می‌آموزیم یا برمی‌داریم

Section titled “آنچه می‌آموزیم یا برمی‌داریم”
  • هیچ کدی قابل استفادهٔ دوباره نیست (فایل مجوز ندارد). فقط ایده‌ها:
  • یافتهٔ مربوط به لایهٔ رویی مفید است: منطقی نگه داشتن بافر و سپردن رسم سطر به یک موتور متن واقعی (HarfBuzz + UAX #9)، RTLِ شکل‌دهی‌شده با ترتیب درست و کپیِ منطقی به دست می‌دهد. این همان مدل داده‌ای درست است. آنچه کم است، چیدمانی لنگرانداخته به خانه‌ها و یک نگاشت دیداری↔منطقی برای آزمون برخورد است.
  • شکاف xterm.js را تأیید می‌کند: نه شکل‌دهی، نه دوجهتی، و نه هیچ API عمومی برای جایگزین کردن رندر سطر. لایهٔ رویی به _core._renderService خصوصی وابسته است (:421-423).
  • bidi-js (MIT، lojjic) به‌عنوان پیاده‌سازیِ قابل‌جاسازیِ UAX #9 کار می‌کند و سطوح، بخش‌های بازچینی و قرینه‌سازی را فراهم می‌کند. گزینه‌ای برای پیشگوهای آزمون (test oracles) ماست.
  • فایل‌های آزمون (*.test.ts) موارد مشخصی را فهرست می‌کنند: لام‌الف همراه با اعراب، سطرهای با جهت مختلط، و رفت‌وبرگشت ANSI.

محدودیت‌ها و الگوهای نادرست

Section titled “محدودیت‌ها و الگوهای نادرست”
  • مسیر B دقیقاً همان چیزی است که اصول ما رد می‌کند: جایگزینی با فرم‌های نمایشی، جریانی که از پیش به ترتیب دیداری وارونه شده، و کدهای ANSI که در افست‌های منطقیِ کهنه دوباره درج می‌شوند (رنگ‌ها روی نویسه‌های اشتباه می‌نشینند). در نتیجه خروجی برای کپی، جست‌وجو و لاگ‌ها خراب می‌شود و هر نشانی‌دهیِ مکان‌نما توسط برنامه‌ها از کار می‌افتد.
  • دو رندر از یک سطر. شبکهٔ بوم و متن DOM در هندسه با هم توافق ندارند، پس مکان‌نما، انتخاب با ماوس و آزمون برخورد پیوندها بر اساس خانه‌های نامرئی کار می‌کنند.
  • لایهٔ رویی روی TUIهای تمام‌صفحه بدون محافظ صفحهٔ جایگزین و با بازسازی کامل DOM در هر نوشتن، به‌علاوهٔ چشمک زدنِ متنِ شکل‌دهی‌نشده که خودِ پروژه به آن اذعان دارد.
  • پاراگراف LTR اجباری (direction:ltr). سطری که تماماً RTL است هرگز راست‌چین نمی‌شود و هیچ تشخیص جهت پایه‌ای (P2/P3) ندارد.
  • داده‌های شکل‌دهیِ فقط عربی (بدون حروف فارسی، بدون پردازش ZWNJ/ZWJ) در همان مسیر کدی که شکل‌دهی را خودش انجام می‌دهد.

یک پروژهٔ مهدی بانشی · mbaneshi.ir