Building Your Own Terminal Emulator with libghostty: A Deep Dive into Ghostling’s Architecture
Ogni giorno apri il terminale decine di volte senza pensarci. Digiti comandi, vedi output colorato, scorri tra migliaia di righe di log. Ma cosa succede realmente tra la pressione di un tasto e i pixel che appaiono sullo schermo?
La risposta coinvolge pseudo-terminali, parsing di escape sequence ANSI, gestione di buffer circolari e un intero sistema di comunicazione inter-processo che risale agli anni ‘70 ma rimane fondamentale nell’infrastruttura moderna.
Ghostling rappresenta un’opportunitΓ unica: un emulatore di terminale minimale costruito su libghostty, la libreria C che alimenta Ghostty, uno dei terminali piΓΉ performanti disponibili oggi. Studiare questa implementazione ti permetterΓ di capire come funziona davvero l’emulazione terminale e, soprattutto, ti darΓ gli strumenti per costruire soluzioni custom: terminali embedded in IDE, tool CLI specializzati o widget terminale per applicazioni desktop.
Prerequisiti
Per seguire questo articolo avrai bisogno di:
- Competenze: familiaritΓ con C e concetti base di sistemi operativi Unix-like (processi, file descriptor)
- Ambiente: Linux o macOS (Windows con WSL2 funziona, ma con limitazioni)
- Strumenti: GCC o Clang, Make, Git
- Librerie: libghostty (la compileremo dai sorgenti)
| |
π‘ Se non hai mai lavorato con PTY o file descriptor, non preoccuparti: spiegherΓ² ogni concetto quando diventa rilevante.
Architettura e concetti chiave
Prima di scrivere codice, devi capire i tre pilastri dell’emulazione terminale:
- PTY (Pseudo-Terminal): il canale di comunicazione bidirezionale tra shell ed emulatore
- State Machine: il parser che interpreta i byte in arrivo (testo, colori, movimento cursore)
- Rendering Pipeline: la trasformazione dello stato interno in pixel sullo schermo
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USER INPUT β
β β
β βββββοΏ½οΏ½οΏ½ββββββ ββββββββββββββββ β
β β Tastiera ββββββββββΆβ Eventi Input β β
β ββββββββββββ ββββββββ¬ββββββββ β
βββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TERMINAL EMULATOR (Ghostling) β
β β
β ββββββββββββββ ββββββββββββββ βββββββββββββββββββ β
β β PTY Master ββββββββΆβ PTY Slave ββββββββΆβ Shell Process β β
β β β β β β (bash/zsh/fish)β β
β βββββββ¬βββββββ ββββββββββββββ ββββββββββ¬βββββββββ β
β β β β
β β Byte Stream β β
β βΌ βΌ β
β βββββββββββββββββββββββ ββββββββββββββββββ β
β β Escape Sequence β β Programmi CLI β β
β β Parser (libghostty) β β (ls, vim, etc) β β
β ββββββββββββ¬βββββββββββ ββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββ β
β β Terminal State β β
β β (colori, cursore) β β
β ββββββββββββ¬βββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββ β
β β Character Grid β β
β β Buffer β β
β ββββββββββββ¬βββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββ β
β β Rendering Pipeline β β
β ββββββββββββ¬βββββββββββ β
βββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β OUTPUT β
β β
β βββββββββββ β
β β Display β β
β βββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Il flusso Γ¨ circolare: l’utente preme un tasto β l’emulatore scrive sul PTY master β il kernel inoltra al PTY slave β la shell legge, elabora e scrive l’output β l’output torna al PTY master β il parser interpreta le escape sequence β lo stato del terminale si aggiorna β il renderer disegna.
Come libghostty si inserisce in questo flusso
libghostty astrae la complessitΓ del parser e della gestione dello stato. Tu ti occupi di:
- Creare e gestire il PTY
- Passare i byte ricevuti a libghostty
- Leggere lo stato risultante e renderizzarlo
libghostty si occupa di:
- Parsing completo delle escape sequence (ANSI, xterm, VT100)
- Gestione del character grid (inserimenti, cancellazioni, scroll)
- Tracking di attributi (colori, grassetto, sottolineato)
- Gestione delle modalitΓ terminale (alternate screen, mouse reporting)
Implementazione passo-passo
Sezione 1: Setup del PTY e fork del processo shell
Il PTY Γ¨ il cuore di tutto. Senza un PTY funzionante, non hai un terminale β hai solo un programma che stampa testo.
| |
β οΈ Attenzione: il codice nel processo figlio dopo
fork()Γ¨ delicato. Non usaremalloc,printfo altre funzioni non async-signal-safe prima diexecl. Possono causare deadlock.
π Nota:
O_NOCTTYnelposix_openptè importante. Senza di esso, il PTY potrebbe diventare il terminale di controllo del processo emulatore, causando comportamenti inaspettati con i segnali.
Sezione 2: Integrazione con libghostty per il parsing
Ora che abbiamo un PTY funzionante, dobbiamo interpretare i byte che arrivano dalla shell. Questo Γ¨ dove libghostty brilla.
β οΈ Nota importante: le API di libghostty mostrate di seguito sono illustrative e basate su pattern comuni nelle librerie di emulazione terminale. Consulta la documentazione ufficiale di libghostty per le firme esatte delle funzioni e le strutture dati.
| |
π‘ Tip:
ghostty_terminal_feedè progettata per gestire dati parziali. Se una escape sequence è troncata a metà (es: ricevi\x1b[ma non il resto), libghostty mantiene lo stato e completa il parsing alla prossima chiamata.
Sezione 3: Il main loop e l’event handling
Ora uniamo i pezzi in un loop principale che gestisce input e output:
| |
β οΈ Non trattare mai il singolo
read()come un messaggio completo: il kernel spezza le escape sequence a metΓ di continuo. Passa tutti i byte aghostty_terminal_feede lascia che sia libghostty a bufferizzare le sequenze parziali.
Con PTY, parser/macchina a stati e renderer collegati, il loop qui sopra Γ¨ l’intera colla: leggi dal master del PTY, dai i byte a libghostty, lascia che sollevi le callback di damage/bell/title, e disegna una volta per frame.
Configurazione per Produzione
Lo scheletro qui sopra Γ¨ un terminale funzionante, ma alcune cose separano una demo da qualcosa che useresti ogni giorno:
- Rasterizzazione dei font reale. Sostituisci il placeholder
cell_width = 8con FreeType o HarfBuzz. Misura advance width e line height alla DPI di destinazione, costruisci un atlas di glifi con chiave(codepoint, bold, italic)e mettilo in cache. Rispetta i flag di larghezza delle celle di libghostty: una cella “wide” (CJK / emoji) occupa due colonne. - Scrollback. libghostty tiene lo schermo attivo; salva le righe espulse in un ring buffer e sposta l’origine di rendering quando l’utente scrolla.
- Resize. Su
SIGWINCHricalcola righe/colonne dalla dimensione in pixel, chiamaghostty_terminal_resize, poiioctl(pty, TIOCSWINSZ, &ws)e rialloca il framebuffer. - Clipboard & OSC 52. Gestisci la sequenza OSC 52 che libghostty espone, così
tmux/nvimremoti possono scrivere nella clipboard di sistema. - Reload della config. Tieni colori, font e keybinding in una struct ricostruibile su
SIGHUPsenza riavviare il PTY.
| |
Errori Comuni e Troubleshooting
Output “sporco” / escape sequence parsate a metΓ . Stai trattando ogni read() come un messaggio completo. Passa ogni byte a ghostty_terminal_feed e non parsare mai il buffer grezzo da solo.
Processi shell zombie. Hai forkato la shell ma non fai waitpid() su SIGCHLD. Installa l’handler, fai il reap del figlio ed esci pulito quando muore.
Mismatch di TERM. Se dichiari TERM=xterm-256color ma non implementi le sequenze che i programmi si aspettano (bracketed paste, alt-screen, mouse reporting), vim e htop si comportano male. Parti da xterm e dichiara 256color solo quando il path della palette funziona.
L’input non arriva alla shell. Non hai messo il PTY in raw mode, oppure scrivi i tasti sul fd sbagliato: la GUI scrive sull’estremitΓ master, la shell legge dallo slave.
Flicker o tearing. Stai disegnando direttamente su una superficie visibile. Renderizza sul framebuffer offscreen e poi presentalo in un’unica operazione sincronizzata al vblank.
CPU al 100% sugli output flood. Repaint dell’intera griglia a ogni read() del PTY. Accumula i damage, limita a un repaint per frame.
Performance e ScalabilitΓ
- Rendering guidato dal damage: ridisegna solo le righe che libghostty segnala e accorpa gli eventi in un frame. Γ il guadagno piΓΉ grande.
- Cache dell’atlas di glifi: trasforma la rasterizzazione per-cella in un blit di texture; un
draw_glypha freddo Γ¨ ordini di grandezza piΓΉ lento di un lookup in cache. - Batch delle letture dal PTY: buffer da 64 KB e svuota l’fd in un loop prima di disegnare, cosΓ¬ un burst diventa un feed + un frame invece di migliaia.
- Niente allocazioni per-cella nel path di rendering: preallochi framebuffer e glyph cache; il loop caldo non alloca nulla.
- Throttle al refresh del display: un
timerfd(o la frame callback del compositor) limita il lavoro a ~16 ms qualunque sia la velocitΓ del figlio. - Profila con un flood test:
time (yes | head -c 100000000)dentro il tuo terminale deve restare responsive e ben sotto un core.
Conclusioni e Next Steps
Hai costruito un terminale da tre livelli componibili: un PTY che esegue la shell, libghostty che fa il parsing VT e la gestione dello stato dello schermo, e un renderer che ridisegna solo ciΓ² che Γ¨ cambiato. La decisione di design chiave Γ¨ lasciare a libghostty la correttezza β parser, macchina a stati, copertura delle escape sequence β cosΓ¬ il tuo codice Γ¨ solo plumbing e pixel.
Da qui:
- Sostituisci il rasterizzatore placeholder con FreeType + HarfBuzz e un atlas di glifi.
- Aggiungi scrollback, selezione e supporto clipboard OSC 52.
- Collega un windowing reale (GLFW, SDL, o un backend Wayland/X11) e la presentazione accelerata via GPU.
- Implementa mouse reporting e bracketed paste per la piena compatibilitΓ con
tmux/nvim. - Leggi il sorgente di Ghostty per capire come un embedder di produzione gestisce config, ligature e Kitty graphics.