رفتن به محتوا

termux-app-arabic

  • مخزن: https://github.com/kimo0076/termux-app-arabic · کامیت خوانده‌شده: 9b324e9 (یک کامیت فشرده‌شدهٔ واحد، ۲۰۲۶-۰۶-۲۱، «Termux with Arabic BiDi rendering + UI features») · مجوز: GPLv3-only به ارث رسیده از LICENSE.md بالادستی (بدون تغییر)؛ پروژهٔ بالادستی terminal-view/terminal-emulator را به‌عنوان استثنای Apache-2.0 و termux-shared را عمدتاً MIT فهرست کرده است. GitHub فقط به این دلیل NOASSERTION نشان می‌دهد که فایل مجوز به‌صورت نثر در Markdown نوشته شده است. · زبان: Java · سکو: Android
  • دسته: شبیه‌ساز ترمینال (فورک)

فورکی از termux/termux-app که یک رندرکنندهٔ ردیفِ آگاه از دوجهتی (bidi) به terminal-view اضافه می‌کند. شکل‌دهی (shaping) گلیف‌های عربی به پشتهٔ متنی خود اندروید سپرده شده است (Canvas.drawTextRun، یعنی Minikin + HarfBuzz). README همان README بالادستی Termux است و هیچ اشاره‌ای به این قابلیت نمی‌کند. ادعاهای تبلیغاتی («UAX #9 + HarfBuzz»، «prompt protection»، «cursor isolation»، «cached layout») فقط در رابط کاربری برنامه آمده‌اند: app/src/main/res/values/strings.xml:240-246 (بخش What’s New) و SettingsActivity.java:128-139 (بخش About، همراه با نام توسعه‌دهنده). مخزن هیچ کد بومی (native)، هیچ اتصالی (binding) به HarfBuzz/ICU/FriBidi و هیچ تغییری در CMake ندارد. تنها کد JNI همان کد بالادستی است (termux.c، termux-bootstrap.c، local-socket.cpp).

این فورک سه فایل کد اضافه می‌کند و یک ویرایش بزرگ انجام می‌دهد:

  • terminal-view/.../view/textrender/BiDiTextHelper.java (۵۸ خط): تشخیص بازه‌های نقطه‌کد عربی.
  • terminal-view/.../view/textrender/ArabicTextShaper.java (۳۰۵ خط): یک بازشکل‌دهنده (reshaper) مبتنی بر Presentation-Forms-B. هیچ‌جا فراخوانی نمی‌شود (کد مرده).
  • terminal-view/.../view/TerminalRenderer.java: یک مسیر جدید چیدمان و رندر برای ردیف‌هایی که عربی دارند.
  • افزوده‌های صرفاً رابط کاربری: WhatsNewActivity.java و رشته‌ها.

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

Section titled “چگونه کار می‌کند (تأییدشده در کد)”
  1. ماشه (دروازهٔ ابتکاری). TerminalRenderer.render ردیفی را فقط وقتی به renderRowBidi می‌فرستد که BiDiTextHelper.containsRtlCharacters نویسه‌ای در بازه‌های U+0600–06FF، 0750–077F، 08A0–08FF، FB50–FDFF یا FE70–FEFF پیدا کند (TerminalRenderer.java:134، BiDiTextHelper.java:44-50). ردیف‌های عبری، سریانی، تانا و انکو کاملاً از دوجهتی رد می‌شوند، با این‌که توضیحات کد از «RTL» حرف می‌زنند. هر ردیف دیگری از مسیر بدون‌تغییر بالادستی می‌رود.
  2. پاراگراف = یک ردیف فیزیکی صفحه، با پایهٔ اجباری LTR. کد new Bidi(text, Bidi.DIRECTION_LEFT_TO_RIGHT) را روی نویسه‌های منطقی ردیف فراخوانی می‌کند (:336-337). بلوک توضیحات در :311-335 استدلالش را می‌آورد: ترمینال یک رابط LTR است، اعلان (prompt) و نویسه‌های خنثای شل در سطح 0 می‌مانند، و هیچ تلاشی برای رده‌بندی اعلان/خروجی نمی‌شود. این UAX #9 واقعی است (java.text.Bidi، که در اندروید بر پایهٔ ICU است)، اما فقط با یک سطح پایهٔ ثابت. نه تشخیص خودکار P2/P3 در کار است، نه کنترل به‌ازای هر برنامه، و نه رسیدگی به خطوط نرم‌شکسته (soft-wrapped) به‌عنوان یک پاراگراف.
  3. نگاشت منطقی ← سلول ← دیداری. computeLayout (:267-420) در چهار گام کار می‌کند:
    • colAt[charIdx] و colToCharStart/End[col] را از روی WcWidth می‌سازد و نشانه‌های بی‌پهنا را در سلول پایه ادغام می‌کند (:280-309).
    • تکه‌های (runs) دوجهتی را می‌گیرد، پهنای هر تکه را بر حسب ستون می‌سنجد و Bidi.reorderVisually را روی تکه‌ها فراخوانی می‌کند، نه روی سلول‌ها (:341-362).
    • تکه‌ها را از چپ به راست در ستون‌های دیداری می‌چیند.
    • هر تکه را به تکه‌های سبکی تقسیم می‌کند. برای تکه‌های RTL، موقعیت تکه را درون جعبهٔ تکه قرینه می‌کند (segVisCol = V + (le - segLe)، :395) و logicalToVisualCol[] را پر می‌کند (:404-408). هر حرف عربی یک سلول پهنا دارد (WcWidth تغییری نکرده است).
  4. شکل‌دهی. هر تکهٔ دیداری با یک فراخوانی canvas.drawTextRun(text, start, count, 0, contextCount=charsUsed, x, y, isRtl, paint) رسم می‌شود (:601-603). چون زمینهٔ شکل‌دهی کل خط منطقی است، پیوستگی حروف با تقسیم‌شدن سبک‌ها از بین نمی‌رود. isRtl با روش حدسی بازهٔ نقطه‌کد دوباره محاسبه می‌شود، نه از زوج/فرد بودن سطح دوجهتی (frunIsRtl محاسبه می‌شود ولی هرگز به جایی داده نمی‌شود). مسیر دوجهتی mes = widthCols * mFontWidth را می‌فرستد (:471-473)، پس شاخهٔ مقیاس‌دهی برای جا شدن در :551-557 هرگز اجرا نمی‌شود. عربیِ شکل‌داده‌شده با پیشروی (advance) طبیعیِ HarfBuzz رسم می‌شود و ممکن است از جعبهٔ سلولی‌اش بیرون بزند یا آن را کامل پر نکند.
  5. «جداسازی مکان‌نما» (cursor isolation). در هر دو مسیر، مکان‌نما دیگر درون تکهٔ متن رسم نمی‌شود. بعداً به‌صورت یک لایهٔ رویی رسم می‌شود: drawCursorOverlay (:624-689) یک مستطیل می‌کشد و برای مکان‌نماهای بلوکی، متن همان یک سلول را با زمینهٔ کامل خط دوباره رسم می‌کند. هدف این است که تکه در محل مکان‌نما شکسته نشود، چون این کار فرم پیوستگی گلیف را عوض می‌کند («پرپر زدن» یا flicker). در مسیر دوجهتی، ستون لایهٔ رویی logicalToVisualCol[cursorX] است (:478-482)، پس مکان‌نما موقعیت دیداری سلول منطقی را دنبال می‌کند.
  6. «چیدمان کش‌شده» (cached layout). یک WeakHashMap<TerminalRow, RowLayout> با یک عکس‌فوری (snapshot) از نویسه‌ها و تعداد ستون‌ها مقایسه می‌شود (:52، :268-272). دوجهتی فقط وقتی متن ردیف تغییر کند دوباره اجرا می‌شود.
  7. «محافظت از اعلان» (prompt protection) سازوکار جداگانه‌ای نیست. صرفاً همان سطح پایهٔ اجباری LTR در گام ۲ است.
  8. انتخاب و کپی. هایلایت در سطح تکه انجام می‌شود: اگر یک تکهٔ دیداری با گسترهٔ دیداری انتخابِ منطقی هم‌پوشانی داشته باشد، کل آن تکه معکوس‌رنگ می‌شود (:447-458، :470). تست برخورد (hit-testing) لمسی و ماوسی بدون تغییر است (TerminalView.java:546-553). نگاشت پیکسل ← ستون هرگز از طریق چیدمان دوجهتی معکوس نمی‌شود، پس در ردیف‌های بازچینی‌شده یک ضربه یا کشیدن به سلول منطقی اشتباهی اشاره می‌کند. گزارش‌های ماوسی که به برنامه‌ها فرستاده می‌شوند همان ستون‌های دیداریِ نگاشت‌نشده را حمل می‌کنند. کپی از TerminalBuffer.getSelectedText بالادستی استفاده می‌کند که بافر منطقی را می‌خواند: متن کپی‌شده به ترتیب منطقی می‌ماند، اما ممکن است بازهٔ انتخاب‌شده اشتباه باشد.
  9. تست‌ها: هیچ تستی اضافه نشده. هر ۱۹ فایل تست در terminal-emulator/src/test از بالادستی‌اند و terminal-view اصلاً پوشهٔ تست ندارد.
لایه وضعیت شواهد
۱ رمزگذاری و یکسان‌سازی ❌ تغییری نکرده؛ بافر همان‌طور که دریافت شده ذخیره می‌شود
۲ ورودی (چیدمان‌ها، نیم‌فاصله، IME) ❌ هیچ تغییری در IME، صفحه‌کلید یا کلیدهای اضافه برای RTL نیست
۳ پهنا ◐ WcWidth بالادستی بدون تغییر است (حروف ۱ سلول، اعراب ۰). در مسیر دوجهتی پیشروی شکل‌داده‌شده با سلول‌ها جفت نمی‌شود (:471-473)، پس برنامه و ترمینال بر سر تعداد سلول‌ها توافق دارند ولی گلیف‌ها از نظر دیداری جابه‌جا می‌شوند
۴ شکل‌دهی ✅ HarfBuzz اندروید از طریق drawTextRun با زمینهٔ کامل خط (:589-603)؛ در مسیر فعال از فرم‌های نمایشی (presentation forms) استفاده نمی‌شود
۵ دوجهتی ◐ UAX #9 واقعی (java.text.Bidi) برای هر ردیف فیزیکی، با پایهٔ اجباری LTR. ترمینال همیشه مالک دوجهتی است: نه کلید تغییر حالت، نه راه خروج برای برنامه‌هایی که خودشان خروجی را بازچینی می‌کنند، و نه پشتیبانی از عبری
۶ مکان‌نما، انتخاب و کپی ◐ مکان‌نما از منطقی به دیداری نگاشت می‌شود. تست برخورد و گزارش‌های ماوس معکوس‌نگاشت نمی‌شوند. هایلایت انتخاب در سطح تکهست. کپی منطقی است
۷ قلم‌ها ❌ به جایگزینی قلم سیستمی اندروید تکیه می‌کند؛ رسیدگی‌ای به قلم تک‌فاصلهٔ عربی ندارد
۸ مالتی‌پلکسرها ❌ در نظر گرفته نشده. یک تقسیم tmux/zellij از دید ترمینال یک ردیف است، پس تکه‌های RTL ممکن است از مرز پنل‌ها عبور کنند و در هم ادغام شوند
۹ برنامه‌ها و چارچوب‌های TUI ❌ بی‌قیدوشرط اعمال می‌شود، از جمله در صفحهٔ جایگزین (alternate screen)؛ هیچ کنترلی به سبک DECSET/BDSM ندارد
  • «HarfBuzz» (رشته‌ها/بخش About): فقط به‌طور غیرمستقیم و از طریق پشتهٔ متنی سکوی اندروید درست است. هیچ چیزی بسته‌بندی یا مستقیماً فراخوانی نمی‌شود.
  • «Full BiDi (UAX #9) stability»: الگوریتم UAX #9 است، اما مدل پاراگراف یک تقریب با پایهٔ ثابت LTR و به‌ازای هر ردیف است (بخش محدودیت‌ها را ببینید).
  • «Correct handling of RTL/LTR mixed commands»: در موارد ساده درست است. تست برخورد و انتخاب در ردیف‌های بازچینی‌شده نادرست‌اند.
  • «Cached layout … deterministic»: کلید کش سبک‌ها را نادیده می‌گیرد (بخش محدودیت‌ها را ببینید)، پس خروجی ممکن است کهنه باشد.
  • «Production-grade» (توضیح GitHub): بدون تست، یک کامیت فشرده‌شدهٔ واحد، کد مرده رها شده، و README هرگز به‌روز نشده است.

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

Section titled “آنچه می‌آموزیم یا برمی‌داریم”
  • پاسخ به پرسش «پاراگراف یعنی چه؟» اینجا یک پاراگراف یک ردیف فیزیکی با سطح پایهٔ 0 (LTR) است. پیامد مفیدش مهار است: با پایهٔ LTR، بازچینی فقط درون ستون‌هایی که یک تکهٔ RTL در بر می‌گیرد اتفاق می‌افتد. اعلان‌های LTR، نحو شل و مرزهای کادر در کنار زمینهٔ LTR سلول‌هایشان را حفظ می‌کنند، و شبکهٔ سلول‌ها و آدرس‌دهی مکان‌نمای برنامه دست‌نخورده می‌ماند. این یک پیش‌فرض کم‌هزینه و سازگار با TUI است که ارزش دارد با پیشنهاد Freedesktop/BDSM یعنی «implicit mode, LTR base» سنجیده شود.
  • کل تکه‌ها را بازچینی کن، نه تک‌تک سلول‌ها را. هر تکه را با زمینهٔ منطقی کامل خط شکل بده، آن را در جعبهٔ دیداری تکه رسم کن، و بافر را منطقی نگه دار. این تفکیک درست میان لایهٔ چیدمان و لایهٔ شکل‌دهی است.
  • مکان‌نما را به‌صورت لایهٔ رویی و پس از متن رسم کن. این کار نمی‌گذارد مکان‌نما تکه‌ها را بشکند، که در غیر این صورت با هر حرکت مکان‌نما فرم پیوستگی گلیف عوض می‌شد. ارزش کپی‌کردن دارد.
  • برای هر ردیف یک نگاشت logicalToVisualCol بساز و برای مکان‌نما و انتخاب از آن استفاده کن. همین نگاشت، به‌صورت معکوس، همان چیزی است که تست برخورد لازم دارد.

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

Section titled “محدودیت‌ها و الگوهای نادرست”
  • نویسه‌های خنثی میان متن RTL جذب تکهٔ RTL می‌شوند (قاعدهٔ N1 در UAX #9). مرزی مثل │، | یا فاصله‌هایی که میان دو سلول عربی قرار دارند، جزء تکهٔ RTL می‌شوند و همراه آن معکوس می‌شوند. نمونه‌ها: جدول‌هایی با عربی در ستون‌های مجاور، پنل‌های کنار هم، عربی در دو طرف یک جداکننده.
  • فرض‌های پنهان دربارهٔ ترتیب دیداری (visual order). نگاشت پیکسل ← سلول (getColumnAndRow) و گزارش‌های ماوس چیدمان را نادیده می‌گیرند، پس برنامه ستون‌های دیداری را طوری می‌بیند که انگار منطقی‌اند.
  • کش سبک‌ها را نادیده می‌گیرد. کلید فقط متن به‌علاوهٔ ستون‌هاست (:269-270)، پس تغییر رنگ یا ویژگی روی متنِ بدون تغییر با سبک‌های کهنه رندر می‌شود.
  • بازه‌های سخت‌کدشدهٔ عربی در دو جا جای ویژگی Bidi_Class را گرفته‌اند، و isRtl از این بازه‌ها می‌آید نه از سطح دوجهتی.
  • بازشکل‌دهندهٔ مرده مبتنی بر فرم‌های نمایشی (ArabicTextShaper.java). اگر روزی به کار گرفته شود، نادرست خواهد بود:
    • SHAPED_FORMS با c - 0x0621 اندیس‌گذاری می‌شود (:284)، اما جدول U+063B–063F را جا انداخته است. از TATWEEL به بعد هر حرف به ردیف اشتباهی نگاشت می‌شود (FEH فرم‌های NOON را می‌گیرد، MEEM فرم‌های YEH را)، و U+0646 و بالاتر شکل‌داده‌نشده می‌مانند.
    • حروف فارسی پ چ ژ ک گ ی نگاشت نشده‌اند.
    • لام‌الف وجود ندارد.
    • منطق پیوستن نادرست است (الف به‌عنوان «joins left» علامت خورده، و قابلیت پیوستن از راستِ نویسهٔ بعدی نادیده گرفته می‌شود). این نمونه‌ای روشن از الگوی نادرست جایگزینی با فرم‌های نمایشی است.
  • هیچ مذاکره‌ای میان ترمینال و برنامه نیست. برنامه‌هایی که خودشان خروجی را بازچینی می‌کنند (خروجی پردازش‌شده با fribidi، Emacs، برخی TUIها) دوباره بازچینی می‌شوند، و برنامه‌های تمام‌صفحه نمی‌توانند از آن خارج شوند.
  • خطوط منطقیِ نرم‌شکسته ردیف‌به‌ردیف بازچینی می‌شوند، بدون هیچ زمینهٔ دوجهتی مشترک.

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