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
این پروژه چیست
Section titled “این پروژه چیست”برنامهای حدوداً ۱٬۰۰۰ خطی به زبان 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).
- DCS (
- واحد دوجهتی تکه است، نه خط:
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ی ندارد.
پوشش لایهها
Section titled “پوشش لایهها”| لایه | وضعیت | شاهد |
|---|---|---|
| ۱ رمزگذاری و یکسانسازی | ❌ | 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 هیچ کمکی برای راستبهچپ نمیگیرند |
ادعاشده اما تأییدنشده
Section titled “ادعاشده اما تأییدنشده”- «موتور شکلدهی متن» 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 که هرگز استفاده نمیشود.