رفتن به محتوا

Terminal-ar

  • مخزن: https://github.com/yuossef21/Terminal-ar · کامیت خوانده‌شده: 4d24a47 (2026-08-04، کلون سطحی) · مجوز: در مخزن وجود ندارد. package.json و README می‌گویند «MIT»، اما فایل LICENSE در کار نیست؛ پس استفادهٔ دوباره از آن امن نیست · زبان: JavaScript (Electron) · سکو: در درجهٔ اول Windows، و نیز AppImage/.deb برای Linux (Electron 37 + node-pty)
  • دسته: رندرکنندهٔ لایهٔ رویی (overlay) — یک لایهٔ DOM روی xterm.js درون یک شبیه‌ساز ترمینال مبتنی بر Electron

یک ترمینال Electron (xterm.js 5.5 + node-pty/ConPTY) با زبانه‌ها، پوسته‌ها، افزونه‌ها، پیشنهاد خودکار از روی تاریخچه و رابط کاربری عربی. «پشتیبانی از عربی» در آن یک لایهٔ رویی DOM است (renderer/js/bidi/bidi-renderer.js، ۲۸۷ خط). این لایه بافر xterm را می‌خواند و هر تکهٔ هم‌سبک (run) را به‌صورت یک <span> با موقعیت مطلق رسم می‌کند. سپس موتور متن Chromium شکل‌دهی (shaping) و هر پردازش دوجهتی (bidi) را درون هر span انجام می‌دهد. لایه‌های بوم (canvas) خود xterm برای متن، مکان‌نما (cursor) و انتخاب با opacity: 0 پنهان می‌شوند (renderer/css/app.css:229-234). حدود ۳٫۳ هزار خط JS/CSS/HTML نوشتهٔ خود پروژه است. مخاطب README «CLIهای هوش مصنوعی» است (Claude Code، Codex، OpenCode).

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

Section titled “چگونه کار می‌کند (تأییدشده در کد)”
  • مسیر داده: IPC با نام pty:data ← term.write. وضعیت VT و بافر در اختیار xterm.js است. لایهٔ رویی در هر رویداد onLineFeed/onScroll/onResize/onData کل نمای دید (viewport) را از طریق rAF دوباره رندر می‌کند (bidi-renderer.js:28-33, 98-126).
  • ساختن runها (bidi-renderer.js:129-161): خانه‌های پشت‌سرهم با کلید سبک یکسان یک run می‌سازند. با گزینهٔ mergeRuns، نقاط کد RTL مجاور از روی مرز تغییر رنگ هم ادغام می‌شوند تا واژه‌های عربی در یک گرهٔ متنی بمانند. این پیش‌شرطِ اتصال حروف توسط مرورگر است.
  • جای‌گذاری (:200-223): هر run یک <span> می‌شود در left = startCol * cellW (ستون منطقی آن)، با width = cells * cellW و overflow: hidden. جهت span از BidiUtil.dirOfText به‌صورت dir = rtl|ltr|auto تعیین می‌شود (با شمارش اکثریت، bidi-util.js:52-62). بنابراین بازچینی دوجهتی فقط درون یک span رخ می‌دهد. runهای یک سطر هرگز نسبت به هم بازچینی نمی‌شوند و هیچ جهت پاراگرافِ سطح‌سطری روی متن اعمال نمی‌شود.
  • شکل‌دهی: به‌طور کامل به Chromium (HarfBuzz درون Blink) سپرده شده است، چون span متن منطقی واقعی را در خود دارد. هیچ کد یا کتابخانهٔ شکل‌دهی در مخزن نیست. پهنای گلیف‌ها (glyph) همان پیشروی طبیعی قلم است که با overflow: hidden به اندازهٔ جعبهٔ خانه بریده می‌شود.
  • دسته‌بندی دوجهتی (renderer/js/bidi/bidi-util.js): بازه‌هایی که دستی نوشته شده‌اند. LTR_RANGES تمام بازهٔ U+0020–007E را در بر می‌گیرد (:25)، پس فاصله، ارقام و نشانه‌های نگارشی LTR قوی حساب می‌شوند که طبق UAX #9 نادرست است. firstStrongDir (:65-72) به نام قاعدهٔ P2 نام‌گذاری شده اما همان خطا را به ارث می‌برد. isJoinableArabic / NON_JOINERS (:78-85) تعریف شده ولی در رندر استفاده نمی‌شود. این بخش فقط مخصوص عربی است (حروف نچسبِ فارسی مانند U+0698 ژ را ندارد).
  • مکان‌نما (:236-262): اگر جهتِ نخستین نویسهٔ قوی در سطر مکان‌نما RTL باشد، visualX = cols - 1 - cursorX (یعنی مکان‌نما در کل سطر قرینه می‌شود). اما متن در عرض سطر قرینه نمی‌شود (spanها در ستون‌های منطقی می‌مانند)، پس مکان‌نما و گلیف‌ها با هم ناسازگارند. همچنین :246 بدون افزودن baseY تابع buf.getLine(buf.cursorY) را می‌خواند، پس به محض وجود تاریخچهٔ پیمایش (scrollback) سطر اشتباهی را بررسی می‌کند.
  • انتخاب و کپی: لایهٔ رویی pointer-events: none است (app.css:241)، پس انتخاب با ماوس همان انتخاب بومیِ xterm بر پایهٔ خانه‌های منطقی است، اما لایهٔ برجسته‌سازی آن هنگام روشن بودن RTL پنهان است. copy() از term.getSelection() استفاده می‌کند (renderer/js/app.js:375-380)، پس متن کپی‌شده به ترتیب منطقی است. هندسهٔ (نامرئیِ) انتخاب با آنچه spanهای RTL نشان می‌دهند مطابقت ندارد.
  • ورودی (renderer/js/bidi/input-engine.js): کلیدها از طریق term.onData مستقیم به PTY می‌روند. این ماژول فقط بافری از متن تایپ‌شده نگه می‌دارد تا پیشنهاد خودکار از تاریخچه را (به‌صورت متن شبح‌گونه روی لایهٔ رویی) نشان دهد. هیچ کاری در زمینهٔ IME، نیم‌فاصله (ZWNJ) یا یکسان‌سازی انجام نمی‌دهد و به textarea پنهانِ xterm.js تکیه دارد.
  • پهنا: پیش‌فرض xterm.js (wcwidth یونیکد ۶). هیچ چیز سفارشی‌ای ندارد.
  • محیط PTY (main/pty-manager.js:39-42): TERM=xterm-256color، و LANG به‌طور پیش‌فرض en-US.UTF-8 است (نام محلی‌سازیِ بدشکل).
  • کلید روشن/خاموش: Ctrl+Shift+B لایهٔ رویی را پنهان می‌کند و به xterm.js ساده برمی‌گردد.
  • آزمون‌ها: هیچ. نه فایل آزمونی دارد و نه اسکریپت آزمون.
لایه وضعیت شاهد
۱ رمزگذاری و یکسان‌سازی ❌ چیزی فراتر از UTF-8 خودِ xterm.js نیست. پیش‌فرض LANG نادرست است
۲ ورودی ❌ ورودی بی‌واسطه به xterm.js می‌رود. input-engine.js فقط تاریخچه و پیشنهاد خودکار را انجام می‌دهد
۳ پهنا ❌ wcwidth خودِ xterm.js. گلیف‌های لایهٔ رویی در جعبهٔ خانه‌ها بریده یا کشیده می‌شوند و با آن‌ها هماهنگ نمی‌شوند
۴ شکل‌دهی ◐ Chromium/HarfBuzz از طریق متن DOM: اتصال واقعی حروف، اما فقط درون یک span هم‌سبک (یا RTLِ ادغام‌شده)، بریده‌شده به پهنای خانه‌های منطقی
۵ دوجهتی ◐ UAX #9 در Chromium فقط درون هر span، با dir اکتشافی برای span (شمارش اکثریت، و نشانه‌های نگارشی ASCII به‌عنوان LTR قوی). بازچینی در سطح سطر وجود ندارد
۶ مکان‌نما، انتخاب و کپی ◐ کپی منطقی است (getSelection در xterm). قرینه‌سازی مکان‌نما با متن رندرشده ناسازگار است. برجسته‌سازی انتخاب پنهان است
۷ قلم‌ها ◐ جایگزینیِ زنجیرهٔ قلم در CSS ("Noto Sans Arabic","Segoe UI")، که مرورگر مدیریتش می‌کند
۸ مالتی‌پلکسرها ❌ پرداخته نشده (زبانه‌ها و تقسیم‌بندی پنجرهٔ خودش را دارد)
۹ برنامه‌ها و چارچوب‌های TUI ◐ spanها در ستون‌هایی که برنامه انتخاب کرده می‌مانند، پس چیدمان‌های جعبه‌ای عمدتاً سالم می‌مانند. اما قرینه‌سازی مکان‌نما در سطرهایی که با RTL شروع می‌شوند و بریدگی گلیف‌ها TUIها را خراب می‌کند
  • «Full Arabic Bidi & Shaping» / «Chromium implements full UAX #9» (docs/ARCHITECTURE.md:40): فقط درون تک‌تک spanها. سطر هرگز به‌عنوان یک پاراگراف دوجهتی در نظر گرفته نمی‌شود و سطرهای دارای جهت‌های مختلط ترتیب منطقیِ runها را حفظ می‌کنند.
  • «تراز مطلق برای ابزارهای TUI (Claude Code، OpenCode، Vim، htop) بدون اعوجاج»: هیچ کد مخصوص TUI وجود ندارد. تراز فقط به این دلیل برقرار می‌ماند که runها به ستون‌های منطقی سنجاق شده‌اند، و مکان‌نمای قرینه‌شده در عرض سطر آن را نقض می‌کند.
  • ARCHITECTURE.md یک خط ورودی هوشمند مبتنی بر textarea با تشخیص prompt و IME را توصیف می‌کند (:47-55). کد خلاف این را می‌گوید: «no intermediate field» (input-engine.js:5-8، و setOverlayMode به‌اجبار روی 'off'). مستندات از کد عقب افتاده است.
  • مجوز MIT: در README و package.json ذکر شده، اما فایل LICENSE وجود ندارد، و homepage در package.json به سازمان دیگری اشاره می‌کند.
  • فایل‌های اجرایی انتشار (Terminal-AR-Setup-1.0.0.exe، AppImage): از روی کد منبع قابل تأیید نیستند.

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

Section titled “آنچه می‌آموزیم یا برمی‌داریم”
  • هیچ کدی قابل استفادهٔ دوباره نیست (فایل مجوز ندارد). فقط ایده‌ها.
  • الگوی «لایهٔ رویی روی یک شبیه‌ساز دست‌نخورده» ارزان‌ترین راه برای رسیدن به عربیِ متصل در یک میزبان Electron/xterm.js است. ترفند کلیدی آن ادغام خانه‌های RTL مجاور از روی مرز سبک‌هاست تا مرورگر کل واژه را شکل‌دهی کند. این هم به‌عنوان مبنای سریعی برای یک افزونهٔ xterm.js و هم به‌عنوان نمونه‌ای عبرت‌آموز مفید است.
  • ایدهٔ یک لایهٔ RTL قابل روشن/خاموش کردن برای هر نشست (Ctrl+Shift+B) راه گریز معقولی از نظر تجربهٔ کاربری است.
  • نگه داشتن کپی روی بافر منطقیِ شبیه‌ساز، کپیِ منطقیِ درست را بی‌هزینه به دست می‌دهد.

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

Section titled “محدودیت‌ها و الگوهای نادرست”
  • دو منبع حقیقت برای چیدمان. شبکهٔ پنهان xterm آزمون برخورد (hit-testing) و انتخاب را پیش می‌برد، در حالی که لایهٔ رویی DOM چیز دیگری رسم می‌کند. مکان‌نما، انتخاب و گلیف‌ها ممکن است با هم ناسازگار باشند.
  • دوجهتیِ اکتشافی با کلاس‌های نادرست: فاصله، ارقام و نشانه‌های نگارشی ASCII به‌عنوان LTR قوی در نظر گرفته می‌شوند، و جهت span با رأی اکثریت تعیین می‌شود.
  • بدون دوجهتیِ سطح‌سطری: یک سطر مختلط فارسی/انگلیسی ترتیب منطقیِ runها را حفظ می‌کند (مثلاً فرمانی LTR که پس از آن آرگومانی RTL می‌آید، در یک پاراگراف RTL به ترتیب نادرست خوانده می‌شود). با این حال مکان‌نما در سطرهایی که با RTL شروع می‌شوند همچنان در کل سطر قرینه می‌شود.
  • بریدن گلیف‌هایی با پهنای طبیعی در جعبهٔ خانه‌ها (overflow:hidden) واژه‌های عریض عربی/فارسی را بی‌صدا کوتاه می‌کند.
  • کل نمای دیدِ DOM با هر تکهٔ خروجی PTY از نو ساخته می‌شود، که در خروجی‌های سنگین هزینهٔ کارایی دارد.
  • از جنبهٔ مثبت، هیچ جایگزینی با فرم‌های نمایشی (presentation forms) انجام نمی‌دهد و بافر را به ترتیب دیداری (visual order) بازنویسی نمی‌کند.
  • بلوغ: یک کامیت سطحی. بدون آزمون، بدون TODO، و با مستنداتی که با کد تناقض دارد. به نظر می‌رسد پوستهٔ برنامه‌ای پرامکانات با یک لایهٔ RTLِ نازک و اکتشافی باشد.

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