Interactive Compatibility (Milestone 14)
The final milestone: run real programs, find what breaks, name the missing capability, and fix it. This is how terminal emulators are actually developed — the spec is enormous and mostly irrelevant, and the programs tell you which parts matter.
Document which features break and identify the missing terminal capability.
The discipline is in that second clause. "vim looks weird" is not a bug report. "The status line scrolls away because DECSTBM is not implemented" is.
The Test Programs
| Program | Exercises | Typical first failure |
|---|---|---|
bash | Line editing, readline, job control, SIGWINCH | Arrow keys (DECCKM), or Alt+key word movement |
zsh | The same, plus a right-hand prompt and syntax highlighting | RPROMPT positioning; heavy SGR use |
vim | Alternate screen, scroll regions, cursor addressing, mouse, focus events, bracketed paste | The alternate screen, or scroll regions |
less | Alternate screen, scroll regions, \r overwriting, search highlighting | Scroll regions; -X behaves differently |
top | Scroll regions, cursor addressing, full-screen redraw at 1 Hz | The header scrolling away |
htop | All of top, plus mouse reporting and 256 colors | Mouse drag (?1002) |
| Python REPL | Readline, multi-line editing, \r handling, exceptions with color | Continuation-line rendering |
ssh | Everything at once, plus latency, plus a second terminal's opinions | Split escape sequences; TERM propagation |
man | Alternate screen, overstrike bold (X\bX), pagination | Overstrike rendering |
git log --graph | Unicode box drawing, color, pagination through less | Box-drawing widths |
tmux | Being a terminal inside your terminal; every capability at once | Nested-terminal capability mismatches |
The Compatibility Matrix
Build this table. It is the deliverable.
| Program | Feature | Status | Missing capability | Fixed |
|---|---|---|---|---|
| vim | Alternate screen | ✅ | — | — |
| vim | Scroll regions | ✅ | — | — |
| vim | Mouse selection | ❌ | ?1002 drag reporting | #47 |
| vim | Bracketed paste | ✅ | — | — |
| vim | Focus events | ❌ | ?1004 | #52 |
| top | Header stays put | ✅ | DECSTBM | — |
| htop | Mouse click | ✅ | ?1000 + ?1006 | — |
| htop | Mouse drag | ⚠️ | ?1002 — partial | #47 |
| less | Search highlight | ✅ | SGR 7 (inverse) | — |
| man | Bold via overstrike | ❌ | X\bX → bold | #61 |
| bash | Alt+B word back | ✅ | ESC prefix | — |
| zsh | Right prompt | ⚠️ | Cursor save/restore under wrap | #63 |
| ssh + vim | Everything | ⚠️ | Split-sequence handling under latency | #58 |
| git log --graph | Box drawing | ✅ | — | — |
| tmux (nested) | Everything | ⚠️ | ?2026 synchronized output | #70 |
Legend: ✅ works · ⚠️ partially · ❌ broken
The Method
For each program, a four-step loop:
1. RUN IT. In your terminal, doing something real for two minutes.
Not a smoke test — actually use it.
2. OBSERVE. What is wrong? Be specific:
"the status line scrolls away"
not "top looks broken"
3. IDENTIFY. Which capability is missing?
Record the session; grep for sequences you do not handle:
mini-term replay --format debug session.cast 2>&1 \
| grep -i unhandled | sort | uniq -c | sort -rn
4. FIX + REGRESS. Implement it, and add a golden case from that recording so
it can never break again.
Step 3 is where the leverage is. Log every unhandled sequence — that log is your to-do list, ordered by frequency.
#![allow(unused)] fn main() { fn debug_unhandled(&mut self, private: Option<u8>, inter: &[u8], action: char, params: &Params) { if !self.config.log_unhandled { return; } // Aggregate rather than spamming: you want a frequency table, not a log. *self.unhandled.entry(format!("CSI {}{}{}", private.map(|b| b as char).unwrap_or(' '), String::from_utf8_lossy(inter), action)).or_insert(0) += 1; } }
mini-term run --log-unhandled --timeout 5000 -- vim -c 'q' 2>&1 | tail -20
# 12 CSI ?1004 h ← focus reporting: 12 occurrences, implement first
# 3 CSI ?2026 h ← synchronized output
# 1 CSI > 4 ; 2 m ← modifyOtherKeys
Known Breakages and Their Causes
The failures you will actually hit, in roughly the order you will hit them:
| Symptom | Missing capability | Where to fix |
|---|---|---|
vim quits and leaves its buffer in scrollback | Alternate screen (?1049) | Modes |
top's header scrolls away | DECSTBM | Screen Model |
| A blank line after every full-width line | Pending wrap | Screen Model |
Arrow keys type ^[[A in vim | DECCKM not honored in the encoder | Input Encoding |
Pasting into vim staircases | Bracketed paste (?2004) | Modes |
Clicking in htop does nothing | Mouse modes ?1000/?1006 | Lab 11 |
Dragging in htop does nothing | ?1002 | Same |
man bold text looks like XX | Overstrike (X\bX) not rendered as bold | The print path |
| CJK text misaligns box drawing | Width policy disagreement | UTF-8 and Graphemes |
zsh's right prompt is in the wrong place | Cursor save/restore, or pending wrap | CSI Catalog |
| A program hangs at startup | It sent a query (CSI 6n, CSI c, OSC 11;?) you never answered | Reply handling |
Colors are wrong only in ls | Bold-brightening policy | SGR and Color |
Everything breaks over ssh, intermittently | Split escape sequences | The parser is not a real state machine |
tmux inside your terminal flickers | Synchronized output (?2026) | Modes |
vim does not reload changed files | Focus reporting (?1004) | Modes |
Overstrike bold: the one that surprises people
`man` (via groff and less) produces bold by OVERSTRIKING:
'X' BS 'X' → bold X
'_' BS 'X' → underlined X
This predates SGR by decades — it made a printing teletype strike the
character twice. Terminals still implement it, and if you do not, man pages
render as "XX" and "_X".
Implementation, in the print path:
if the character being printed EQUALS the character already in the cell
and the cursor arrived there via BS → set BOLD instead of overwriting.
if the existing cell is '_' and BS brought us here → set UNDERLINE and
write the new character.
The TERM and terminfo Decision
You must decide what TERM your terminal sets, and be honest about it.
| Option | Consequence |
|---|---|
TERM=xterm-256color | Programs assume every xterm capability. Anything you have not implemented breaks in a way that looks like your bug. The pragmatic default. |
TERM=xterm | Fewer assumptions, but you also lose 256 colors |
TERM=vt100 | Very conservative; no color, no mouse; almost nothing modern works |
A custom TERM=mini-terminal with your own terminfo entry | Correct, but every user must have your terminfo installed, and over ssh the remote host will not — so it falls back to something worse |
The recommendation: ship TERM=xterm-256color, and maintain an explicit list of xterm
capabilities you do not implement, with what breaks for each.
## Capabilities we claim (TERM=xterm-256color) but do not implement
| Capability | terminfo | What breaks | Plan |
|---|---|---|---|
| Sixel graphics | — | Image display in `timg`, `chafa` | Not planned |
| Blinking text | `blink` | SGR 5 renders static | By design; blinking is hostile |
| Double-width lines | `DECDWL` | `banner`-style output renders single-width | Not planned |
| Charset shifting G2/G3 | `S2`/`S3` | Rare legacy programs | Not planned |
| `DECRQSS` | — | Programs cannot query current SGR | Planned |
| Bracketed paste query | — | Programs guess instead of asking | Planned |
That table is the honest version of "we are xterm-compatible", and writing it is part of Milestone 14.
The ssh Test
ssh deserves its own section because it breaks things nothing else does.
ssh localhost
# inside: vim, top, tmux — everything, over a real (if local) network stack
What ssh changes | Consequence |
|---|---|
| Latency | Escape sequences split across read() calls at arbitrary points. If your parser is not a real state machine, this is where it fails — intermittently, which is the worst kind. |
TERM propagation | The remote host's terminfo database may not have your TERM. It falls back, often to something much worse. |
| Window size | ssh propagates SIGWINCH over the connection. If your resize handling is wrong, the remote program renders at the wrong size. |
| Two line disciplines | Your local PTY and the remote one. ^C handling gets subtle. |
| A second terminal's opinions | The remote tmux or screen has its own width table and capability set. |
# Deliberately add latency to expose split-sequence bugs (Linux):
sudo tc qdisc add dev lo root netem delay 200ms
ssh localhost
# run vim, use it. Escape should still feel responsive if you got the timeout right.
sudo tc qdisc del dev lo root netem
That tc experiment is the single best way to find parser chunking bugs, and it takes thirty seconds.
The Session Log
For each program, record a session and keep it. It becomes both documentation and a golden case.
for prog in "vim -u NONE -c q" "top -n 2" "less -XF /etc/services" \
"htop -d 5" "man ls" "git log --graph --oneline -20"; do
name=$(echo "$prog" | tr ' /' '__')
mini-term run --rows 24 --cols 80 --timeout 5000 \
--record "tests/golden/cases/${name}.cast" \
--log-unhandled -- bash -c "$prog" 2>>"/tmp/unhandled.log"
done
sort /tmp/unhandled.log | uniq -c | sort -rn > /tmp/todo.txt
cat /tmp/todo.txt
/tmp/todo.txt is your Milestone 14 backlog, ordered by how much real software needs each feature.
Experiment
CLAIM. Every compatibility break has a specific missing capability, and the terminal itself will tell you which one.
METHOD.
# 1. Pick a program that misbehaves.
mini-term run --log-unhandled --record broken.cast --timeout 10000 -- htop
# 2. Ask what it wanted.
mini-term replay --format debug broken.cast 2>&1 | grep -i unhandled | sort | uniq -c | sort -rn
# 3. Cross-check against a working terminal: what does IT receive?
# Run htop in xterm, recording with `script`, and diff the sequence sets.
script -q -c 'htop -d 5' /tmp/xterm-htop.raw
# (quit after a few seconds)
mini-term replay --format debug --from-raw /tmp/xterm-htop.raw 2>&1 \
| grep -oE 'CSI [^ ]+' | sort -u > /tmp/xterm-seqs.txt
mini-term replay --format debug broken.cast 2>&1 \
| grep -oE 'CSI [^ ]+' | sort -u > /tmp/mine-seqs.txt
diff /tmp/xterm-seqs.txt /tmp/mine-seqs.txt
PREDICTION. Before running: which capability will appear most often in the unhandled log? Will the
sequence sets differ between terminals, and if so, why would htop send different sequences to two
terminals?
RESULT. That last question has an interesting answer: programs query capabilities (CSI c,
DECRQM, XTGETTCAP) and adapt. If you answer queries differently, programs behave differently —
which means some compatibility bugs are caused by your replies, not your rendering.
Test
#![allow(unused)] fn main() { /// Interactive tests are marked #[ignore] so they do not slow the normal suite. /// Run with: cargo test -- --ignored #[test] #[ignore] fn vim_starts_and_quits_cleanly() { let out = mini_term_timeout(&["--rows", "24", "--cols", "80"], &["vim", "-u", "NONE", "-c", "q"], 5000); // vim uses the alternate screen; on exit the primary must be restored, so // the snapshot must NOT contain vim's tildes. assert!(!out.contains("~\n~\n~"), "alt screen was not restored:\n{out}"); } #[test] #[ignore] fn top_header_does_not_scroll_away() { let out = mini_term_timeout(&["--rows", "24", "--cols", "80"], &["top", "-b", "-n", "3"], 5000); assert!(out.lines().next().unwrap().contains("top -"), "the header must remain on row 0 (DECSTBM)"); } #[test] #[ignore] fn man_renders_overstrike_as_bold() { let out = mini_term_debug_timeout(&["--rows", "24", "--cols", "80"], &["man", "ls"], 5000); assert!(!out.contains("NNAAMMEE"), "overstrike must become bold, not doubled text"); assert!(out.contains("BOLD"), "some cell should carry the bold attribute"); } #[test] #[ignore] fn no_program_leaves_unhandled_sequences_above_a_threshold() { // A ratchet: as you implement capabilities, tighten the threshold. It // prevents regression AND documents progress. for prog in &["vim -u NONE -c q", "top -b -n 2", "less -XF /etc/hosts"] { let unhandled = count_unhandled(prog); assert!(unhandled <= 3, "{prog}: {unhandled} unhandled sequences"); } } #[test] #[ignore] fn ssh_to_localhost_running_vim_works() { // The hardest integration test: two PTYs, a network stack, and latency. let out = mini_term_timeout(&["--rows", "24", "--cols", "80"], &["ssh", "-o", "StrictHostKeyChecking=no", "localhost", "TERM=xterm-256color vim -u NONE -c q"], 15000); assert!(!out.contains("Warning"), "{out}"); } }
Challenge Extensions
- Implement overstrike bold and verify with
man lsthatNNAAMMEEbecomes boldNAME. - Implement
?2026(synchronized output) and measure tearing inneovimwith and without. - The latency experiment with
tc netemat 50/200/500 ms, runningvimat each. Any breakage is a parser chunking bug. - Write and install a real terminfo entry, then compare program behavior with your entry versus
xterm-256color. Document what changes. - Automate the compatibility matrix: a script that runs every program, counts unhandled sequences, and regenerates the table. Commit the table so its diff shows progress.
- Implement
XTGETTCAPso programs can query capabilities directly, and observe which ones start behaving differently. - Test nested: your terminal →
tmux→ssh→tmux→vim. Four layers. Everything that breaks is a real capability mismatch, and this is what users actually do.
Deliverables
- The compatibility matrix, filled in for all eleven programs.
- The unhandled-sequence frequency table from real sessions.
- At least three bugs found by real programs, fixed, with a regression golden case each.
-
The honest capability table: what you claim via
TERMand do not implement. -
vim,less,top,htop,bash,zsh, the Python REPL, andsshall working. - Wide characters and combining marks verified with real CJK and emoji text.
-
The differential suite against
vtepassing, with divergences documented. -
The
ssh-with-latency test performed.
Validation / Self-check
- Name the four steps of the compatibility method, and which one has the leverage.
- For each of the eleven programs, name the capability it exercises most.
- What is overstrike bold, why does it exist, and what breaks without it?
- Which
TERMshould you set, and what obligation does that create? - Why does
sshbreak things nothing else does? Name five reasons. - How do you find out which capability a program wanted?
- Why might a program send different sequences to two different terminals?
- What does the unhandled-sequence ratchet test buy you?
- Name five known breakages and the missing capability for each.
- Your terminal works perfectly locally but garbles
vimoversshintermittently. What is the bug, and how do you reproduce it deterministically?
Next: The terminal-debugger.