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

ProgramExercisesTypical first failure
bashLine editing, readline, job control, SIGWINCHArrow keys (DECCKM), or Alt+key word movement
zshThe same, plus a right-hand prompt and syntax highlightingRPROMPT positioning; heavy SGR use
vimAlternate screen, scroll regions, cursor addressing, mouse, focus events, bracketed pasteThe alternate screen, or scroll regions
lessAlternate screen, scroll regions, \r overwriting, search highlightingScroll regions; -X behaves differently
topScroll regions, cursor addressing, full-screen redraw at 1 HzThe header scrolling away
htopAll of top, plus mouse reporting and 256 colorsMouse drag (?1002)
Python REPLReadline, multi-line editing, \r handling, exceptions with colorContinuation-line rendering
sshEverything at once, plus latency, plus a second terminal's opinionsSplit escape sequences; TERM propagation
manAlternate screen, overstrike bold (X\bX), paginationOverstrike rendering
git log --graphUnicode box drawing, color, pagination through lessBox-drawing widths
tmuxBeing a terminal inside your terminal; every capability at onceNested-terminal capability mismatches

The Compatibility Matrix

Build this table. It is the deliverable.

ProgramFeatureStatusMissing capabilityFixed
vimAlternate screen✅——
vimScroll regions✅——
vimMouse selection❌?1002 drag reporting#47
vimBracketed paste✅——
vimFocus events❌?1004#52
topHeader stays put✅DECSTBM—
htopMouse click✅?1000 + ?1006—
htopMouse drag⚠️?1002 — partial#47
lessSearch highlight✅SGR 7 (inverse)—
manBold via overstrike❌X\bX → bold#61
bashAlt+B word back✅ESC prefix—
zshRight prompt⚠️Cursor save/restore under wrap#63
ssh + vimEverything⚠️Split-sequence handling under latency#58
git log --graphBox 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:

SymptomMissing capabilityWhere to fix
vim quits and leaves its buffer in scrollbackAlternate screen (?1049)Modes
top's header scrolls awayDECSTBMScreen Model
A blank line after every full-width linePending wrapScreen Model
Arrow keys type ^[[A in vimDECCKM not honored in the encoderInput Encoding
Pasting into vim staircasesBracketed paste (?2004)Modes
Clicking in htop does nothingMouse modes ?1000/?1006Lab 11
Dragging in htop does nothing?1002Same
man bold text looks like XXOverstrike (X\bX) not rendered as boldThe print path
CJK text misaligns box drawingWidth policy disagreementUTF-8 and Graphemes
zsh's right prompt is in the wrong placeCursor save/restore, or pending wrapCSI Catalog
A program hangs at startupIt sent a query (CSI 6n, CSI c, OSC 11;?) you never answeredReply handling
Colors are wrong only in lsBold-brightening policySGR and Color
Everything breaks over ssh, intermittentlySplit escape sequencesThe parser is not a real state machine
tmux inside your terminal flickersSynchronized output (?2026)Modes
vim does not reload changed filesFocus 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.

OptionConsequence
TERM=xterm-256colorPrograms assume every xterm capability. Anything you have not implemented breaks in a way that looks like your bug. The pragmatic default.
TERM=xtermFewer assumptions, but you also lose 256 colors
TERM=vt100Very conservative; no color, no mouse; almost nothing modern works
A custom TERM=mini-terminal with your own terminfo entryCorrect, 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 changesConsequence
LatencyEscape 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 propagationThe remote host's terminfo database may not have your TERM. It falls back, often to something much worse.
Window sizessh propagates SIGWINCH over the connection. If your resize handling is wrong, the remote program renders at the wrong size.
Two line disciplinesYour local PTY and the remote one. ^C handling gets subtle.
A second terminal's opinionsThe 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

  1. Implement overstrike bold and verify with man ls that NNAAMMEE becomes bold NAME.
  2. Implement ?2026 (synchronized output) and measure tearing in neovim with and without.
  3. The latency experiment with tc netem at 50/200/500 ms, running vim at each. Any breakage is a parser chunking bug.
  4. Write and install a real terminfo entry, then compare program behavior with your entry versus xterm-256color. Document what changes.
  5. 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.
  6. Implement XTGETTCAP so programs can query capabilities directly, and observe which ones start behaving differently.
  7. 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 TERM and do not implement.
  • vim, less, top, htop, bash, zsh, the Python REPL, and ssh all working.
  • Wide characters and combining marks verified with real CJK and emoji text.
  • The differential suite against vte passing, with divergences documented.
  • The ssh-with-latency test performed.

Validation / Self-check

  1. Name the four steps of the compatibility method, and which one has the leverage.
  2. For each of the eleven programs, name the capability it exercises most.
  3. What is overstrike bold, why does it exist, and what breaks without it?
  4. Which TERM should you set, and what obligation does that create?
  5. Why does ssh break things nothing else does? Name five reasons.
  6. How do you find out which capability a program wanted?
  7. Why might a program send different sequences to two different terminals?
  8. What does the unhandled-sequence ratchet test buy you?
  9. Name five known breakages and the missing capability for each.
  10. Your terminal works perfectly locally but garbles vim over ssh intermittently. What is the bug, and how do you reproduce it deterministically?

Next: The terminal-debugger.