ทำให้ภาษาไทยในเทอร์มินัล Herdr อ่านง่าย: รวมฟอนต์เอง 2 รอบ กว่าจะได้แบบที่ใช่
ผมใช้ Herdr เป็นที่รวมเทอร์มินัลกับ coding agent ทุกตัว มันดีทุกอย่าง ยกเว้นเวลาเจอภาษาไทย ญ ฌ ตัวเล็กเท่ามด ระยะห่างตัวอักษรไม่เท่ากัน บางคำวรรณยุกต์ลอยไปผิดที่ วันหนึ่งเลยถาม Claude Code (ที่รันอยู่ใน Herdr นั่นแหละ) ว่าแก้ได้ไหม และมีใครแจ้งปัญหานี้หรือยัง
ผลคือต้องรวมฟอนต์เองสองรอบ รอบแรกถูกต้องทุกข้อแต่อ่านยาก รอบสองถึงได้แบบที่ใช่ ด้านบนคือก่อน ด้านล่างคือหลัง:
ขั้นแรก: หาว่าปัญหาอยู่ตรงไหน
เทอร์มินัลใน Herdr แบ่งเป็นสองส่วน herdr-core เก็บสิ่งที่อยู่บนจอ ส่วนแอปเป็นคนวาด พออ่านจอจาก core ด้วย hc read ข้อความไทยครบทุกตัว แปลว่าข้อมูลไม่เสีย ปัญหาอยู่ตอนวาด
ต้นเหตุคือ JetBrains Mono ซึ่งเป็นฟอนต์เทอร์มินัลตั้งต้นไม่มีตัวไทยเลย แอปจึงไปหยิบ Noto Sans Thai มาแทน ฟอนต์นี้ออกแบบมาให้แต่ละตัวกว้างไม่เท่ากัน ญ กว้างเกือบช่องครึ่ง พอต้องยัดลงช่องเดียวก็โดนบีบจนเล็ก ส่วนตัวแคบอย่าง า ก็ลอยอยู่กลางช่อง ดูห่างๆ
แล้วจะแก้ทางไหน settings.json ของ Herdr ตั้งได้แค่ font ตัวเดียว ไม่มีที่บอกว่า “ตัวไทยให้ใช้ฟอนต์นี้” ค้นใน issue ของ herdr-app, herdr และ herdr-core ก็ไม่มีใครแจ้งเรื่องภาษาไทยไว้เลย Claude ร่าง issue ภาษาอังกฤษพร้อมภาพหน้าจอมาให้ แต่ผมยังไม่โพสต์ เพราะมีคำถามค้างอยู่ในใจก่อน
คำถามที่ค้างใจ: ภาษาไทยเข้ากับช่องสี่เหลี่ยมได้จริงเหรอ
ผมสงสัยว่าเทอร์มินัลที่แบ่งทุกอย่างเป็นช่องเท่าๆ กันอาจไม่เหมาะกับภาษาไทยตั้งแต่ต้น หรือควรให้ ญ กินสองช่อง หรือต้องใช้ลิเกเจอร์กับไม้หันอากาศหรือสระอิ
คำตอบกลับตรงข้ามกับที่คิด ภาษาไทยเข้ากับระบบช่องได้ดีกว่าที่คิดมาก
- พยัญชนะ สระหน้า (เ แ โ ใ ไ) สระ า ะ ใช้คนละหนึ่งช่อง
- สระบน สระล่าง และวรรณยุกต์ (ั ิ ี ุ ู ่ ้ ๊ ๋ ็ ์) กว้างศูนย์ช่อง วาดซ้อนบนพยัญชนะในช่องเดียวกันอยู่แล้ว ไม่ต้องใช้ลิเกเจอร์
- ตัวที่ยุ่งจริงมีตัวเดียวคือ ำ ซึ่งนับเป็นหนึ่งช่อง แต่วงกลมต้องลอยขึ้นไปอยู่บนพยัญชนะตัวหน้า
- พิมพ์ดีดไทยก็เป็นแบบช่องเท่ากันมาเป็นร้อยปี
ส่วนการให้ ญ กินสองช่อง ทำไม่ได้ เพราะทุกโปรแกรมในเทอร์มินัล (bash, vim, Claude Code) นับเองว่าตัวไหนกว้างกี่ช่อง และนับ ญ เป็นหนึ่ง ถ้าแอปวาดเป็นสองช่อง เคอร์เซอร์กับข้อความที่เหลือทั้งบรรทัดจะเลื่อนเพี้ยน
สรุปคือระบบช่องไม่ผิด ที่ผิดคือฟอนต์ที่ไม่ได้ออกแบบมาให้พอดีช่อง ทางแก้จึงเป็นการทำฟอนต์ที่มีทั้งตัวอังกฤษของ JetBrains Mono และตัวไทยที่ปรับให้พอดีช่อง รวมไว้ในไฟล์เดียว แล้วตั้งเป็น font ของ Herdr
รอบแรก: Tlwg Mono ถูกต้องทุกข้อ แต่อ่านยาก
ตัวเลือกแรกคือ Tlwg Mono ของโครงการ TLWG ซึ่งออกแบบมาให้เป็น monospace ภาษาไทยอยู่แล้ว ทุกตัวกว้าง 600 หน่วยเท่ากับ JetBrains Mono พอดี ใช้ fontTools คัดลอกตัวไทยทั้งหมดเข้าไปใน JetBrains Mono ขยาย 10% ให้สูงใกล้ตัวอังกฤษ แล้วปรับสระบนสระล่างกับวรรณยุกต์ให้กว้างศูนย์และขยับไปซ้อนบนตัวหน้า
ผลออกมาถูกต้องทุกข้อ ญ ฌ ขนาดเต็มตัว คอลัมน์ตรง วรรณยุกต์อยู่ถูกที่
แต่พอใช้จริงวันรุ่งขึ้น ผมรู้สึกว่า “อ่านยากครับ ฟอนต์เดิมอ่านง่ายกว่า” ตัวไทยของ Tlwg Mono เส้นบางและหน้าตาแบบพิมพ์ดีด ส่วน Noto ถึงจะโดนบีบ ก็ยังออกแบบมาให้อ่านบนจอ เลยถอดกลับเป็นแบบเดิมก่อน
รอบสอง: Noto Sans Thai Looped ใส่ช่องแบบไม่ให้เสียหน้าตา
รอบนี้ผมเลือกเอง ใช้ Noto Sans Thai Looped (แบบมีหัว) ความยากคือ Noto ไม่ได้ออกแบบมาให้เท่ากันทุกตัว ต้องจัดเองทีละตัว
- ตัดเอาแต่ภาษาไทย โดยเก็บกฎการจัดตำแหน่งของ Noto ไว้ด้วย (
ccmp,locl,mark,mkmk) กฎพวกนี้คือสิ่งที่ทำให้วรรณยุกต์ขยับขึ้นเมื่อมีสระบนอยู่ก่อน เช่นใน “ที่” “กิ่ง” - ใส่ทุกตัวลงช่องเดียว ย่อตัวไทยลงเหลือ 95% แล้ววางกลางช่อง 600 หน่วย ตัวที่ยังกว้างเกินช่องอย่าง ญ ฌ ณ ให้แคบลงเฉพาะแนวนอน ความสูงคงเดิม จึงไม่เล็กเท่ามดอีก
- ย้ายจุดยึดตาม สระและวรรณยุกต์ของ Noto วางตำแหน่งด้วยจุดยึด (anchor) บนพยัญชนะ พอพยัญชนะถูกขยับหรือบีบ จุดยึดต้องขยับตามด้วยสูตรเดียวกัน ไม่อย่างนั้นวรรณยุกต์จะลอยไปอยู่ข้างๆ
- รวมเข้ากับ JetBrains Mono แล้วตั้งชื่อฟอนต์ใหม่ว่า “JetBrains Mono Thai”
ระหว่างทางเจอปัญหาสองเรื่อง
- ระยะบรรทัดกว้างขึ้น ตอนรวมฟอนต์ fontTools เลือกค่าความสูงบรรทัดที่มากกว่า ซึ่งเป็นของ Noto ทั้งเทอร์มินัลเลยห่างขึ้น แก้โดยบังคับให้ใช้ค่าของ JetBrains Mono
- “ฤๅ” ทำให้ ๅ หายไปหนึ่งช่อง Noto มีลิเกเจอร์ (
rlig) ที่รวม ฤ กับ ๅ เป็นตัวเดียว ในหน้าเว็บนี่สวย แต่ในเทอร์มินัลสองตัวต้องใช้สองช่อง พอรวมเหลือช่องเดียว ทุกอย่างหลังจากนั้นก็เลื่อน แก้โดยตัดลิเกเจอร์นี้ทิ้ง
ผลคือภาพ “หลัง” ด้านบนสุด ตัวไทยมีหัว เส้นหนาใกล้ตัวอังกฤษ ญ ฌ ณ สูงเต็มตัว วรรณยุกต์อยู่ถูกที่ และทุกตัวอยู่ช่องละตัว
เรื่องที่ควรจำไว้
hc readแยกได้ว่าปัญหาอยู่ที่ข้อมูลหรืออยู่ที่การวาด ถ้าข้อความจาก core ครบ ก็ไม่ต้องไปไล่ที่ encoding- Herdr โหลดรายชื่อฟอนต์ตอนเปิดแอปเท่านั้น ติดตั้งฟอนต์ใหม่หรือเปลี่ยน
fontแล้วต้องปิดเปิดใหม่ ระหว่างนั้น terminal กับ agent ยังทำงานอยู่ใน herdr-core ไม่หยุด herdr-app shotถ่ายได้แค่เฟรมล่าสุด ถ้าหน้าต่างอยู่ใน workspace ที่ไม่ได้เปิด แอปไม่วาดเฟรมใหม่ ภาพที่ได้จะเป็นของเก่า- ฟอนต์ที่รวมเองไม่ได้อัปเดตตาม ถ้า JetBrains Mono ออกเวอร์ชันใหม่ ต้องรันสคริปต์สร้างใหม่
สิ่งที่ยังไม่ได้ทำ
ยังไม่ได้แจ้งปัญหาไปที่ Herdr ทางแก้ที่ยั่งยืนกว่าคือให้ Herdr มีการตั้งค่าฟอนต์สำรอง ทุกคนที่ใช้ภาษาไทยจะได้ไม่ต้องรวมฟอนต์เอง ฟอนต์นี้ก็ยังทดสอบกับ Herdr บน Linux เครื่องเดียว
ลองใช้ดู
สคริปต์สร้างฟอนต์อยู่ใน gist นี้ (build-merge.py) ต้องมี JetBrains Mono และ Noto Sans Thai Looped ในเครื่องก่อน:
uv run --with fonttools python build-merge.py \
JetBrainsMonoNerdFont-Regular.ttf NotoSansThaiLooped-Regular.ttf \
JetBrainsMonoThai-Regular.ttf Regular /tmp ทำซ้ำกับ Bold, Italic, Bold Italic แล้ววางไฟล์ใน ~/.local/share/fonts ตั้ง "font": "JetBrains Mono Thai" ใน ~/.config/herdr-app/settings.json แล้วปิดเปิด Herdr ใหม่ครับ ใช้กับเทอร์มินัลตัวอื่นที่ตั้งฟอนต์ได้ทีละตัวก็ได้เหมือนกัน
— Ben