เทอร์มินัลแห่งอนาคต
(jyn.dev)- ชี้ให้เห็นถึง ความซับซ้อนและข้อจำกัดของสถาปัตยกรรมเทอร์มินัลแบบเดิม พร้อมนำเสนอแนวคิดเทอร์มินัลยุคถัดไปที่รวมการป้อนข้อมูล การแสดงผล และการควบคุมโปรเซสเข้าไว้ด้วยกันในรูปแบบใหม่
- ใช้ Jupyter Notebook เป็นต้นแบบ เพื่อสำรวจความเป็นไปได้ของ อินเทอร์เฟซแบบโต้ตอบ เช่น การเรนเดอร์ภาพ การรันคำสั่งซ้ำ การแก้ไขผลลัพธ์ได้ และเอดิเตอร์แบบฝังในตัว
- อธิบายอย่างเป็นรูปธรรมผ่านกรณีของ Warp และ iTerm2 ถึง การผสานรวมเชิงลึกระหว่างเชลล์กับเทอร์มินัล (shell integration) การจัดการโปรเซสที่รันยาวนาน และความสามารถในการแยก/กู้คืนเซสชัน
- วางภาพฟีเจอร์ต่อยอดบนพื้นฐานของ การติดตาม dataflow (dataflow tracking) และ ความคงอยู่ถาวร (persistence) เช่น undo/redo ของคำสั่ง การรันซ้ำอัตโนมัติ และเทอร์มินัลสำหรับการทำงานร่วมกัน
- เสนอ กลยุทธ์การพัฒนาแบบค่อยเป็นค่อยไป จาก CLI เชิงทรานแซกชัน → เซสชันแบบคงอยู่ → RPC แบบมีโครงสร้าง → ฟรอนต์เอนด์สไตล์ Jupyter
โครงสร้างพื้นฐานของเทอร์มินัล
- เทอร์มินัลประกอบด้วย 4 ส่วน: terminal emulator, virtual terminal (PTY), shell, และ process group
- terminal emulator คือโปรแกรมที่ เรนเดอร์โครงสร้างแบบกริด บนหน้าจอ
- PTY คือสถานะภายในเคอร์เนล ทำหน้าที่ส่งอินพุตไปยัง process group และแปลงสัญญาณ (signal)
- shell ทำหน้าที่เป็น event loop ที่อ่านและพาร์สอินพุต รวมถึงสร้างโปรเซส
- โปรเซสต่าง ๆ โต้ตอบกับองค์ประกอบเหล่านี้ผ่านอินพุตและเอาต์พุต
- อินพุตไม่ได้เป็นเพียงข้อความธรรมดา แต่รวมถึง สัญญาณ (signal) ด้วย ส่วนเอาต์พุตประกอบด้วย ANSI escape sequence ที่ใช้แสดงการจัดรูปแบบ
ภาพของเทอร์มินัลที่ดีกว่า
- เทอร์มินัลแบบเดิมมีข้อจำกัดด้านฟังก์ชันมาก ทำให้ขาดทั้ง ความสามารถในการขยาย และ ความโต้ตอบ
- Jupyter Notebook มีฟีเจอร์ที่ terminal emulator แบบ VT100 ดั้งเดิมทำไม่ได้
- การเรนเดอร์ภาพความละเอียดสูง
- ปุ่ม “รันใหม่ตั้งแต่ต้น” ที่แทนที่ผลลัพธ์เก่าโดยไม่เติมต่อท้าย
- “มุมมอง” ที่สามารถเขียนทับทั้งซอร์สโค้ดและผลลัพธ์ได้ในตำแหน่งเดิม (เช่น แสดง Markdown เป็นซอร์สหรือเป็น HTML ที่เรนเดอร์แล้ว)
- เอดิเตอร์ในตัวที่มี syntax highlighting, tabs, panels และรองรับเมาส์
- แต่แนวคิด Jupyter Notebook ที่ ใช้เชลล์เป็นเคอร์เนล ก็เจอหลายปัญหา
- เชลล์รับคำสั่งทีเดียวทั้งก้อน ทำให้ tab completion, syntax highlighting และ autosuggestion ใช้งานไม่ได้
- ปัญหาการจัดการโปรเซสที่รันนาน: Jupyter โดยพื้นฐานจะรันจนกว่าเซลล์จะเสร็จ สามารถยกเลิกได้ แต่ ไม่สามารถหยุดชั่วคราว ดำเนินต่อ โต้ตอบ หรือดูโปรเซสที่กำลังรันอยู่ได้
- ปุ่ม “รันเซลล์ใหม่” อาจสร้างปัญหากับสถานะของเครื่อง (โดยเฉพาะเมื่อมีคำสั่งอย่าง
rm -rf) - การ undo/redo ใช้งานไม่ได้
แล้วมันจะทำงานอย่างไร?
-
การผสานรวมเชลล์ (Shell Integration)
- เทอร์มินัล Warp สร้าง การผสานรวมแบบเนทีฟ ระหว่างเทอร์มินัลกับเชลล์
- เทอร์มินัลเข้าใจจุดเริ่มต้นและจุดสิ้นสุดของแต่ละคำสั่ง รวมถึงเอาต์พุตและอินพุตของผู้ใช้
- ทำได้โดยใช้ฟังก์ชันมาตรฐาน (custom DCS)
- iTerm2 ก็ใช้แนวทางคล้ายกัน โดยรองรับ OSC 133 escape code
- ไปยังคำสั่งก่อนหน้า/ถัดไปได้ด้วยคีย์ลัดเดียว
- แจ้งเตือนเมื่อคำสั่งทำงานเสร็จ
- ถ้าเอาต์พุตเลื่อนหลุดจอ จะแสดงคำสั่งปัจจุบันเป็น “overlay”
- เทอร์มินัล Warp สร้าง การผสานรวมแบบเนทีฟ ระหว่างเทอร์มินัลกับเชลล์
-
การจัดการโปรเซสที่รันนาน
- การโต้ตอบ (interacting) :
- หากต้องการโต้ตอบกับโปรเซสที่ทำงานนาน จำเป็นต้องมี การสื่อสารสองทาง
- ตัวอย่าง TUI:
top,gdb,vim - Jupyter เด่นในด้านการออกแบบ เอาต์พุตแบบโต้ตอบ ที่เปลี่ยนแปลงและอัปเดตได้
- ตัวอย่าง TUI:
- ฟีเจอร์เทอร์มินัลที่คาดหวัง: มี “free input cell” ให้ใช้งานตลอดเวลา
- โปรเซสแบบโต้ตอบรันอยู่ด้านบนของหน้าต่าง และมี input cell อยู่ด้านล่าง
- หากต้องการโต้ตอบกับโปรเซสที่ทำงานนาน จำเป็นต้องมี การสื่อสารสองทาง
- การหยุดชั่วคราว (suspending) :
- การ “พัก” โปรเซสเรียกว่า job control
- เทอร์มินัลยุคใหม่ควรแสดง สถานะของโปรเซสที่ถูกพักและโปรเซสเบื้องหลังแบบต่อเนื่องทางสายตา
- คล้ายกับที่ IntelliJ แสดง “กำลังทำดัชนี...” ที่แถบงานด้านล่าง
- การตัดการเชื่อมต่อ (disconnecting) : มี 3 แนวทางสำหรับการแยกและกู้คืนเซสชัน
- Tmux / Zellij / Screen: แทรก terminal emulator เพิ่มอีกชั้นระหว่าง terminal emulator กับโปรแกรม เซิร์ฟเวอร์เป็นเจ้าของ PTY และเรนเดอร์เอาต์พุต ขณะที่ไคลเอนต์นำเอาต์พุตไปแสดงใน terminal emulator จริง สามารถแยกไคลเอนต์ เชื่อมต่อใหม่ หรือเชื่อมหลายไคลเอนต์พร้อมกันได้ iTerm สามารถทำตัวเป็นไคลเอนต์ของตัวเองโดยข้าม tmux client และสื่อสารกับเซิร์ฟเวอร์โดยตรง
- Mosh: ทางเลือกแทน SSH รองรับการเชื่อมต่อกลับเข้าสู่เซสชันเทอร์มินัลหลังเครือข่ายหลุด เซิร์ฟเวอร์รัน state machine แล้วเล่นซ้ำความต่างแบบ incremental ของ viewport ไปยังไคลเอนต์ โดยคาดหวังให้ terminal emulator จัดการ multiplexing และ scrollback เอง เนื่องจากไคลเอนต์รันอยู่ฝั่งเครือข่ายจริง การแก้ไขบรรทัดแบบโลคัลจึงตอบสนองได้ทันที
- alden/shpool/dtach/abduco/diss: จัดการเฉพาะการแยก/กลับมาใช้เซสชันด้วยโมเดลไคลเอนต์/เซิร์ฟเวอร์ ไม่รวมเครือข่ายหรือ scrollback และไม่มี terminal emulator ของตัวเอง จึงมี ระดับการแยกที่สูงกว่า เมื่อเทียบกับ tmux และ mosh
- การโต้ตอบ (interacting) :
-
การรันซ้ำและการย้อนกลับ
- คำตอบคือ การติดตาม data flow
- ปัจจุบัน pluto.jl ทำสิ่งนี้ได้โดยเชื่อมเข้ากับคอมไพเลอร์ Julia
- อัปเดตเซลล์ที่พึ่งพาเซลล์ก่อนหน้าแบบเรียลไทม์
- หาก dependency ไม่เปลี่ยน ก็จะไม่อัปเดตเซลล์
- เป็น Jupyter ที่คล้ายสเปรดชีต ซึ่งจะรันโค้ดใหม่เฉพาะเมื่อจำเป็น
- และสามารถทำให้เป็นทั่วไปมากขึ้นด้วย orthogonal persistence
- แซนด์บ็อกซ์โปรเซสและติดตาม I/O ทั้งหมด เพื่อป้องกันสิ่งที่ “ประหลาดเกินไป” ตราบใดที่โปรเซสไม่สื่อสารกับโปรเซสอื่นนอกแซนด์บ็อกซ์
- ทำให้มองโปรเซสได้ว่าเป็นฟังก์ชันบริสุทธิ์ของอินพุต โดยอินพุตคือ “ทั้งระบบไฟล์ ตัวแปรสภาพแวดล้อมทั้งหมด และคุณสมบัติของโปรเซสทั้งหมด”
ฟีเจอร์ที่ต่อยอดได้
- ต้องมีฟรอนต์เอนด์แบบ Jupyter:
- Runbooks (จริง ๆ แล้วสร้างได้ด้วยเพียง Jupyter และ PTY primitives)
- การปรับแต่งเทอร์มินัลด้วย CSS ปกติ โดยไม่ต้องใช้ภาษาปรับแต่งเฉพาะหรือ ANSI color code แปลก ๆ
- การค้นหาคำสั่งจากเอาต์พุต/เวลา: ตอนนี้ค้นหาได้ทั้งในเอาต์พุตทั้งหมดของเซสชันปัจจุบันหรือในประวัติอินพุตคำสั่งทั้งหมด แต่ยังไม่มี smart filter และเอาต์พุตก็ไม่คงอยู่ข้ามเซสชัน
- ต้องมีการผสานรวมเชลล์:
- timestamp และเวลาในการรันของแต่ละคำสั่ง
- การแก้ไขบรรทัดแบบโลคัลแม้จะข้ามขอบเขตเครือข่าย
- IntelliSense สำหรับคำสั่งเชลล์โดยไม่ต้องกด Tab พร้อมการเรนเดอร์ที่ผสานอยู่ในเทอร์มินัล
- ต้องมีการติดตามแซนด์บ็อกซ์:
- ทุกความสามารถของการติดตามแซนด์บ็อกซ์: เทอร์มินัลแบบทำงานร่วมกัน การค้นหาไฟล์ที่ถูกแก้ไขโดยคำสั่ง asciinema ที่แก้ไขได้ระหว่างรันไทม์ และการติดตาม build system
- ขยาย smart search ให้ค้นหาได้ตามสถานะดิสก์ ณ เวลาที่คำสั่งถูกรัน
- ขยาย undo/redo ให้เป็นโมเดลแบบแตกแขนงคล้าย git (emacs undo-tree รองรับแล้ว) พร้อมมุมมองหลายแบบของ process tree
- ด้วยโมเดล undo-tree และการทำแซนด์บ็อกซ์ ทำให้ สามารถให้ LLM เข้าถึงโปรเจกต์และรันหลายตัวแบบขนานพร้อมกันได้ โดยไม่เขียนทับสถานะของกันและกัน พร้อมตรวจสอบ แก้ไข และบันทึกงานเป็น runbook ไว้ใช้ภายหลัง
- เทอร์มินัลที่ตรวจสอบได้เฉพาะสถานะเดิม ในสภาพแวดล้อม production โดยไม่กระทบต่อสถานะของเครื่อง
กลยุทธ์การสร้างแบบเป็นขั้นตอน
-
ขั้นที่ 1: semantics แบบทรานแซกชัน (transactional semantics)
- การเริ่มออกแบบเทอร์มินัลใหม่จาก terminal emulator เป็นแนวทางที่ผิด
- ผู้ใช้ผูกพันกับ emulator ของตน ทั้งการตั้งค่า รูปลักษณ์ และคีย์ไบน์ดิง
- ต้นทุนในการเปลี่ยน emulator สูงมาก
- วิธีที่ถูกต้องคือเริ่มจากเลเยอร์ CLI
- โปรแกรม CLI ติดตั้งและรันง่าย และมี ต้นทุนในการเปลี่ยนต่ำมาก
- ใช้งานแบบครั้งคราวได้โดยไม่ต้องเปลี่ยนเวิร์กโฟลว์ทั้งหมด
- เขียน CLI ที่นำ ความหมายเชิงทรานแซกชันสำหรับเทอร์มินัล มาใช้
- อินเทอร์เฟซอย่าง
transaction [start|rollback|commit] - ทุกอย่างที่รันหลัง
startสามารถย้อนกลับได้ - แค่นี้ก็อาจสร้างธุรกิจได้ทั้งก้อน
- อินเทอร์เฟซอย่าง
- การเริ่มออกแบบเทอร์มินัลใหม่จาก terminal emulator เป็นแนวทางที่ผิด
-
ขั้นที่ 2: เซสชันแบบคงอยู่ (Persistent Sessions)
- หลังมี semantics แบบทรานแซกชันแล้ว ให้ แยกเรื่อง persistence ออกจาก tmux และ mosh
- หากต้องการ persistence ของ PTY จำเป็นต้องใช้โมเดลไคลเอนต์/เซิร์ฟเวอร์
- เคอร์เนลคาดว่า PTY ทั้งสองฝั่งจะเชื่อมต่ออยู่ตลอดเวลา
- สามารถทำอย่างเรียบง่ายโดยใช้คำสั่งอย่าง alden หรือไลบรารีที่คล้ายกัน โดยไม่กระทบ terminal emulator หรือโปรแกรมที่กำลังรันอยู่ในเซสชัน PTY
- เพื่อให้ได้ scrollback เซิร์ฟเวอร์ต้องเก็บ I/O ไว้ไม่จำกัดและ replay ตอนที่ไคลเอนต์เชื่อมต่อใหม่
- terminal emulator จะมี native scrollback ที่ปฏิบัติต่อข้อมูลเหล่านี้เหมือนเอาต์พุตทั่วไป
- สามารถ replay และ resume จากจุดเริ่มใดก็ได้
- ต้องพาร์ส ANSI escape code แต่เป็นงานที่ทำได้หากลงแรงพอ
- สำหรับการกลับมาทำงานต่อของเครือข่ายแบบ mosh ให้ใช้ Eternal TCP (และอาจสร้างบน QUIC เพื่อเพิ่มประสิทธิภาพ)
- แยก persistence ของ PTY ออกจาก persistence ของการเชื่อมต่อเครือข่าย
- Eternal TCP เป็นเพียงการปรับปรุงประสิทธิภาพ: มันสร้างต่อบน bash script ที่วน
ssh host eternal-pty attachก็ได้
- ณ จุดนี้จะสามารถ มีหลายไคลเอนต์เชื่อมต่อกับเซสชันเทอร์มินัลเดียวกันได้ แบบ tmux ขณะที่การจัดการหน้าต่างยังคงเป็นหน้าที่ของ terminal emulator
- หากต้องการการจัดการหน้าต่างแบบรวมศูนย์ terminal emulator อาจใช้โปรโตคอล tmux -CC แบบที่ iTerm ทำ
- ทุกส่วนของขั้นนี้ทำคู่ขนานกับ semantics แบบทรานแซกชันได้อย่างอิสระ แต่เพียงอย่างเดียวก็ยังไม่พอสำหรับการสร้างธุรกิจ
-
ขั้นที่ 3: RPC แบบมีโครงสร้าง
- อาศัยโมเดลไคลเอนต์/เซิร์ฟเวอร์
- เมื่อเซิร์ฟเวอร์คั่นอยู่ระหว่าง terminal emulator กับไคลเอนต์ ก็สามารถทำฟีเจอร์อย่าง การติดแท็ก I/O ด้วยเมทาดาทา ได้
- เพิ่ม timestamp ให้ข้อมูลทุกชิ้นได้
- แยกอินพุตออกจากเอาต์พุตได้
- xterm.js ก็ทำงานคล้ายกัน
- เมื่อรวมกับการผสานรวมเชลล์ ก็สามารถ แยก shell prompt ออกจากเอาต์พุตของโปรแกรมในชั้นข้อมูล ได้
- ทำให้ได้ structured log ของเซสชันเทอร์มินัล
- เล่น log ซ้ำเป็น recording แบบ asciinema
- แปลง shell prompt โดยไม่ต้องรันคำสั่งทั้งหมดใหม่
- นำเข้าไปยัง Jupyter Notebook หรือ Atuin Desktop
- บันทึกคำสั่งแล้วรันซ้ำเป็นสคริปต์ภายหลัง
- เทอร์มินัลกลายเป็นข้อมูล
-
ขั้นที่ 4: ฟรอนต์เอนด์คล้าย Jupyter
- นี่คือ ขั้นแรกที่แตะ terminal emulator โดยตรง และตั้งใจให้เป็นขั้นสุดท้าย
- เพราะมีต้นทุนในการเปลี่ยนสูงที่สุด
- ใช้ประโยชน์จากทุกอย่างที่สร้างมาเพื่อมอบ UI ที่ดี
- CLI
transactionไม่จำเป็นอีกต่อไป เว้นแต่จะต้องการ nested transaction- เพราะทั้งเซสชันเทอร์มินัลเริ่มต้นเป็นทรานแซกชันโดยปริยาย
- เนื่องจากทุกชิ้นส่วนถูกประกอบเข้าด้วยกันแล้ว จึงสามารถมอบฟีเจอร์ต่อยอดทั้งหมดที่กล่าวถึงข้างต้นได้
- นี่คือ ขั้นแรกที่แตะ terminal emulator โดยตรง และตั้งใจให้เป็นขั้นสุดท้าย
บทสรุป
- สถาปัตยกรรมนี้ถูกมองว่า กล้าหาญ ทะเยอทะยาน และอาจใช้เวลาสร้างครบทั้งหมดนานถึงราว 10 ปี
- ควรเดินหน้าอย่างอดทนและค่อยเป็นค่อยไปทีละขั้น
- หวังว่าบทความนี้จะสร้างแรงบันดาลใจให้ใครสักคนเริ่มลงมือสร้างมันด้วยตัวเอง
ยังไม่มีความคิดเห็น