Skip to content

Handle CSI intermediate bytes instead of printing the final byte - #3

Open
kay-ws wants to merge 1 commit into
antmicro:masterfrom
kay-ws:fix-csi-intermediate-bytes
Open

Handle CSI intermediate bytes instead of printing the final byte#3
kay-ws wants to merge 1 commit into
antmicro:masterfrom
kay-ws:fix-csi-intermediate-bytes

Conversation

@kay-ws

@kay-ws kay-ws commented Aug 30, 2026

Copy link
Copy Markdown

Problem

Control sequences containing an intermediate byte have their final byte printed to the screen as a literal character.

printf 'A\e[2 qB\n'

Expected AB, actual AqB. The stray character lands at the cursor position, so an application emitting such a sequence on every cursor movement accumulates it across the line.

Cause

ECMA-48 5.4 defines a control sequence as CSI P...P I...I F, where the Intermediate Bytes I (02/00 to 02/15) "together with the Final Byte F identify the control function".

The state machine has no state for intermediate bytes. On encountering one in EScsi, the lookup in escape_map[] misses and falls through to the default entry of escape_sequences[], which resets the state to ESnormal. The final byte then arrives outside of any sequence and is processed as ordinary text.

This affects every sequence carrying an intermediate byte. ECMA-48 table 4 lists the functions that use a single intermediate byte 02/00 (SL, SR, GSM, ...). A frequently emitted one is DECSCUSR (CSI Ps SP q), used by applications that select a cursor shape per editing mode.

Changes

Add EScsiInter, entered from EScsi on an intermediate byte. It consumes any further intermediate bytes and then the final byte, so nothing reaches the screen. Sequences arriving here are not implemented by fbterm and are discarded silently.

This also keeps CSI 2 SP q (DECSCUSR) distinct from CSI 2 q (DECLL), which is implemented as set_led — the two are different control functions and must not be conflated.

The existing entry for '%' in EScsi, carrying the comment "otherwise the trailing 'm' gets printed", addressed the same problem for a single byte. It is moved into the new state, where the trailing m reaches the SGR handler through EScsiInter. ECMA-48 assigns no function to an intermediate byte followed by 06/13, so this does not shadow a standard sequence.

ESkeep is used as NR_STATES and terminates the section walk in init_state(), so the new state is inserted before it; a comment now records that constraint.

Section numbers in the escape_sequences[] comments were off by one from ESgreater onwards; corrected while adding the new section.

Testing

printf 'expect AB   : A\033[2 qB\n'
printf 'expect RED  : \033[31mRED\033[0%%m NORMAL\n'
printf 'expect CD   : C\033[2qD\n'
before after
A CSI 2 SP q B AqB AB
CSI 0 % m resets SGR resets SGR
C CSI 2 q D (DECLL) CD CD

The second line checks that moving the '%' entry did not regress it; the third checks that DECSCUSR and DECLL remain distinct.

Built and run on x86_64 (Arch, Linux 6.16) and armv7l (Debian bookworm, Linux 3.10), reading /dev/fb0 before and after on both. Colour output, UTF-8 input via uim-fep and scrollback are unaffected.

ECMA-48 5.4 defines a control sequence as "CSI P...P I...I F". The
Intermediate Bytes I (02/00 to 02/15) "together with the Final Byte F
identify the control function", so they can neither be parsed as
parameters nor skipped.

The state machine had no state for them. On encountering an intermediate
byte in EScsi it fell through to the default entry, which resets the
state to ESnormal. The final byte then arrived outside of any sequence
and was printed as a literal character.

For example "CSI 2 SP q" (DECSCUSR) left a stray 'q' on screen at the
cursor position. Applications that select a cursor shape per editing mode
emit it on every cursor movement, so the character accumulated across the
line. ECMA-48 table 4 lists further functions using a single intermediate
byte 02/00 (SL, SR, GSM, ...), which were affected in the same way.

Add EScsiInter so that intermediate bytes are tracked and the final byte
is consumed as part of the sequence. This also keeps "CSI 2 SP q"
(DECSCUSR) distinct from "CSI 2 q" (DECLL), which is implemented as
set_led.

The entry for '%' carrying the comment "otherwise the trailing 'm' gets
printed" addressed the same problem for a single byte. It is moved into
the new state, where the trailing 'm' reaches the SGR handler through
EScsiInter. ECMA-48 assigns no function to an intermediate byte followed
by 06/13, so this does not shadow a standard sequence.

Section numbers in the escape_sequences[] comments were off by one from
ESgreater onwards; corrected while adding the new section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant