رفتن به محتوا

estaterm

  • مخزن: https://github.com/a1-t1/estaterm · کامیت خوانده‌شده: a2c1ce6 (۲۰۲۶-۰۳-۱۷، تنها یک کامیت سطحی (shallow)) · مجوز: Apache-2.0 · زبان: C11 (CMake) · سکو: لینوکس (از <pty.h>/forkpty و -lutil استفاده می‌کند، src/pty.c:7؛ مسیری برای macOS/BSD ندارد)
  • دسته: پراکسی PTY

برنامه‌ای حدوداً ۱٬۰۰۰ خطی به زبان C که پوستهٔ شما را با forkpty اجرا می‌کند و میان آن و ترمینال واقعی می‌نشیند. خروجی برنامهٔ فرزند را بازنویسی می‌کند: متن راست‌به‌چپ را با FriBidi به ترتیب دیداری (visual order) درمی‌آورد و حروف عربی را با فرم‌های نمایشی (presentation forms) جایگزین می‌کند، تا ترمینالی که نه دوجهتی (bidi) دارد و نه شکل‌دهی (shaping)، دست‌کم درست به نظر برسد. همچنین وقتی آخرین خروجی پردازش‌شده راست‌به‌چپ بوده، کلیدهای جهت‌نمای چپ و راست را جابه‌جا می‌کند. این همان ایدهٔ BiCon است، منهای مدل صفحهٔ BiCon.

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

Section titled “چگونه کار می‌کند (تأییدشده در کد)”
  • حلقهٔ پراکسی: روی stdin و سر master در PTY، poll() می‌زند (src/main.c:293-350). بایت‌های stdin در یک «حلقهٔ پژواک» (echo ring) ۵۱۲ بایتی ریخته می‌شوند (main.c:44-82، 316). در مسیر برگشت، بایت‌هایی از خروجی که با پیشوند این حلقه جور درمی‌آیند بدون پردازش عبور می‌کنند، تا متنی که کاربر در حال تایپ آن است جابه‌جا نشود (main.c:133-138).
  • تجزیهٔ دنباله‌های گریز: یک تقسیم‌کنندهٔ دست‌ساز در src/vtparse.c:32-159 خروجی را به تکه‌هایی می‌شکند: CSI (یعنی ESC [، سپس بایت‌های <0x40 و در پایان یک بایت پایانی، :80-98)، OSC (که با BEL یا ESC \ پایان می‌یابد، :101-128)، و هر ESC x دیگری به‌عنوان یک دنبالهٔ گریز دوبایتی (:130-135). سایر بایت‌های C0 به «دنباله‌های گریز» یک‌بایتی تبدیل می‌شوند. CR و LF تکه‌های جداگانه‌اند. همهٔ دنباله‌های گریز عیناً نوشته می‌شوند (main.c:156-159).
    • DCS (ESC P)، APC، PM و SOS را نمی‌شناسد، بنابراین محتوای آن‌ها متن تلقی می‌شود و از دوجهتی و شکل‌دهی عبور می‌کند. محتوای sixel/DCS/tmux-passthrough که متن راست‌به‌چپ داشته باشد خراب می‌شود.
    • باگ بافر (بر پایهٔ خواندن ایستای کد): vt_parser_feed داده را در buf_len اضافه می‌کند و pos=0 را بازنشانی می‌کند (vtparse.c:10-19). buf_len فقط در مسیرهای دنبالهٔ گریز ناقص به عقب برمی‌گردد (:70-73, 86-89, 117-120) و هرگز پس از تخلیهٔ کامل بافر بازنشانی نمی‌شود. اگر کد را تحت‌اللفظی بخوانیم، هر خواندن بعدی بایت‌های قبلی را دوباره پویش و دوباره منتشر می‌کند تا بافر ۸ کیلوبایتی پر شود، و از آن پس دادهٔ تازه بریده می‌شود (:14-15). در هر دو حالت، لایهٔ جریانی (streaming) مقاوم نیست.
    • یک تکهٔ متنی ممکن است در مرز یک read() وسط یک دنبالهٔ UTF-8 تمام شود، و هیچ چیزی پیش از fribidi_charset_to_unicode آن را دوباره سرهم نمی‌کند (bidi.c:13).
  • واحد دوجهتی تکه است، نه خط: bidi_reorder_ex روی هر تکهٔ TEXT اجرا می‌شود (main.c:144). تکه‌ها با هر دنبالهٔ گریز، از جمله تغییر رنگ SGR، از هم جدا می‌شوند (vtparse.c:148-151). در نتیجه یک واژهٔ رنگی، پاراگراف را به تکه‌هایی می‌شکند که هر کدام مستقل جابه‌جا می‌شوند. توصیف «خط‌به‌خط» در README (README.md:172) خوش‌بینانه است.
  • دوجهتی: UAX #9 واقعی از طریق FriBidi با جفت‌کروشه‌ها: fribidi_get_bidi_types، fribidi_get_bracket_types، fribidi_get_par_embedding_levels_ex (پایهٔ FRIBIDI_PAR_ON مگر با --rtl/--ltr)، و سپس fribidi_reorder_line (src/bidi.c:103-154). خط‌هایی که هیچ نویسهٔ راست‌به‌چپی ندارند از مسیر سریع می‌گذرند و بدون تغییر بازگردانده می‌شوند (bidi.c:79-88).
  • شکل‌دهی: fribidi_join_arabic و fribidi_shape(FRIBIDI_FLAGS_DEFAULT | FRIBIDI_FLAGS_ARABIC) پیش از جابه‌جایی اجرا می‌شوند (bidi.c:131-141). این کار کدنقطه‌ها را به فرم‌های نمایشی U+FB50–FDFF / U+FE70–FEFF بازنویسی می‌کند، همان‌طور که توضیح داخل src/shaper.c:7-16 می‌گوید. FRIBIDI_FLAGS_ARABIC شکل‌دهی لیگاتورها را هم شامل می‌شود؛ FriBidi جای خالی مصرف‌شدهٔ لام‌الف را با U+FEFF علامت می‌زند (رفتار FriBidi است، نه این مخزن)، و estaterm هرگز آن را حذف نمی‌کند که خطر جابه‌جایی ستون‌ها را دارد. قرینه‌سازی (mirroring) فقط از راه پرچم‌های پیش‌فرض fribidi_shape انجام می‌شود، پس --no-shape قرینه‌سازی کروشه‌ها را هم خاموش می‌کند.
  • HarfBuzz: در CMakeLists.txt:9,22,28 لینک شده، ولی grep hb_ در src/ هیچ استفاده‌ای پیدا نمی‌کند. بار اضافهٔ بی‌مصرف است.
  • برنامه‌های تمام‌صفحه: passthrough.c:22-49 دقیقاً به‌دنبال CSI ? 47|1047|1049 h/l می‌گردد و تا وقتی صفحهٔ جایگزین (alternate screen) فعال است همهٔ پردازش را دور می‌زند (main.c:129). حالت‌های خصوصی ترکیبی (CSI ?1049;1h) تطبیق نمی‌خورند. خروجی‌های مکان‌نما-محور (cursor-addressed) روی صفحهٔ اصلی (بازترسیم‌های zle/readline، نوارهای پیشرفت، بازنویسی‌های \r) همچنان کورکورانه بازنویسی می‌شوند.
  • کلیدهای جهت‌نما: اگر آخرین تکهٔ پردازش‌شده راست‌به‌چپ تشخیص داده شده باشد، ESC[C↔ESC[D و ESC[1;N C/D جابه‌جا می‌شوند (src/input.c:20-73، main.c:319-323). حالت مکان‌نمای برنامه‌ای SS3 (ESC O C) را نادیده می‌گیرد.
  • آزمون‌ها: src/test_bidi.c خروجی‌ها را چاپ می‌کند و همیشه با «All tests passed.» تمام می‌شود (:122). هیچ assertionی ندارد.
لایه وضعیت شاهد
۱ رمزگذاری و یکسان‌سازی ❌ UTF-8 فرض شده؛ یکسان‌سازی (normalization) ندارد؛ UTF-8 شکسته میان دو خواندن مدیریت نمی‌شود (vtparse.c:148-157)
۲ ورودی ◐ فقط جابه‌جایی کلیدهای چپ/راست (input.c)؛ منطقی برای چیدمان صفحه‌کلید، نیم‌فاصله (ZWNJ) یا IME ندارد
۳ پهنا ❌ منطق پهنا ندارد؛ نویسهٔ پرکنندهٔ لیگاتور (U+FEFF) که در خروجی می‌ماند ممکن است خانه‌ها را از هم‌گامی بیندازد
۴ شکل‌دهی ◐ اتصال عربی FriBidi ← فرم‌های نمایشی (bidi.c:132-141)؛ با وجود لینک‌شدن، از HarfBuzz استفاده نمی‌شود
۵ دوجهتی ◐ UAX #9 واقعی از طریق FriBidi (bidi.c:103-154)، اما در پراکسی روی هر تکهٔ محدود به دنبالهٔ گریز اعمال می‌شود (ترتیب دیداری در جریان خروجی)
۶ مکان‌نما، انتخاب و کپی ❌ بافر ترمینال فرم‌های نمایشی را با ترتیب دیداری نگه می‌دارد ← کپی و جست‌وجو خراب است؛ جابه‌جایی کلیدهای جهت‌نما یک روش حدسی (heuristic) است
۷ قلم‌ها ❌ به این متکی است که قلم ترمینال گلیف‌های FExx را داشته باشد
۸ مالتی‌پلکسرها ❌ در نظر گرفته نشده؛ DCS passthrough خراب می‌شود
۹ برنامه‌ها و چارچوب‌های TUI ◐ فقط دورزدن صفحهٔ جایگزین (passthrough.c)؛ برنامه‌های TUI هیچ کمکی برای راست‌به‌چپ نمی‌گیرند
  • «موتور شکل‌دهی متن» HarfBuzz (README.md:39): لینک شده، هرگز فراخوانی نمی‌شود.
  • «دنباله‌های گریز دست‌نخورده عبور می‌کنند» (README.md:32): برای CSI/OSC/دوبایتی درست است. برای محتوای DCS/APC/PM درست نیست، و باگ مدیریت بافر که بالاتر گفته شد این ادعا را در عمل مشکوک می‌کند.
  • «هر تکه (run) طبق UAX #9 جهت درست را می‌گیرد» برای خط‌های آمیخته (README.md:139): فقط درون یک تکهٔ بدون دنبالهٔ گریز.
  • «با هر ترمینالی که از یونیکد پشتیبانی کند کار می‌کند» (README.md:5): pty.h فقط لینوکسی است. ترمینالی که خودش دوجهتی را انجام می‌دهد (VTE، Konsole، mlterm) متن را دو بار جابه‌جا می‌کند.
  • «اجرای آزمون‌ها: ctest» به‌عنوان اثبات درستی: آزمون هیچ assertionی ندارد.

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

Section titled “آنچه می‌آموزیم یا برمی‌داریم”
  • توالی فراخوانی FriBidi در bidi.c:103-154 مرجعی فشرده و درست برای به‌کارگیری سطوح (levels) در FriBidi است (انواع ← کروشه‌ها ← get_par_embedding_levels_ex). ما پیش از reorder_line/shape متوقف می‌شویم و سطوح را به یک رندرکننده می‌سپاریم.
  • تشخیص صفحهٔ جایگزین به‌عنوان کلید تغییر حالت، نشانه‌ای مفید برای «برنامه مالک چیدمان است» است؛ مثلاً برای خاموش‌کردن دوجهتی ضمنی. البته ما آن را با یک ماشین حالت VT واقعی تجزیه می‌کنیم.
  • ایدهٔ حلقهٔ پژواک دقیقاً نشان می‌دهد چرا یک بازنویس جریان نمی‌تواند ورودی کاربر را از خروجی برنامه تشخیص دهد. استدلال خوبی است برای اینکه دوجهتی را خود ترمینال بر عهده بگیرد، نه یک پراکسی.
  • فهرست «Related Projects» در README آن (mlterm، BiCon، مشخصهٔ BiDi گروه terminal-wg) با مجموعهٔ کارهای پیشین خود ما هم‌خوان است.

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

Section titled “محدودیت‌ها و الگوهای نادرست”
  • جایگزینی با فرم‌های نمایشی (U+FB50–FEFF) در جریان بایت‌ها. این دقیقاً همان چیزی است که اصول ما رد می‌کند.
  • متن با ترتیب دیداری در بافر ترمینال. کپی و چسباندن، جست‌وجو، grep روی تاریخچهٔ پیمایش (scrollback)، صفحه‌خوان‌ها و ثبت خروجی (script، tmux capture) همگی متنِ وارونه و ازپیش‌شکل‌داده‌شده می‌بینند.
  • مرزهای بخش‌های دوجهتی را دنباله‌های گریز SGR و اندازهٔ read() تعیین می‌کنند، نه معنای پاراگراف.
  • مدل صفحه ندارد. خروجی مکان‌نما-محور روی صفحهٔ اصلی و ویرایشگرهای خط بی‌آنکه بداند متن واقعاً کجا می‌نشیند بازنویسی می‌شوند.
  • جابه‌جایی کلیدهای جهت‌نما بر پایهٔ «آخرین خط خروجی راست‌به‌چپ بود» یک روش حدسی سراسری و حالت‌دار است که اشتباه عمل خواهد کرد.
  • آزمونی با assertion ندارد؛ باگ جریانی طبق خوانش کد؛ وابستگی به HarfBuzz که هرگز استفاده نمی‌شود.

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