# Push and Pop Video Attributes (XTPUSHSGR, XTPOPSGR)

> Save the current SGR attributes on a stack, or some of them, and bring them back later.

- **Sequence:** `CSI Pm # {`
- **XTPOPSGR**, Pop Video Attributes: `CSI # }`
- **XTPUSHSGR**, Push Video Attributes: `CSI Pm # p`
- **XTPOPSGR**, Pop Video Attributes: `CSI # q`
- **xterm:** [Functions using CSI](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)

`CSI # {` saves the current [SGR](https://control-codes.page/csi/sgr/index.html.md) attributes, the ones the next
character will be written with, and `CSI # }` brings them back. A program
can change the colors and style for a while and return to whatever was set
before, without having to know what that was. xterm keeps the saved
attributes on a stack, so pushes and pops nest. `CSI # p` and `CSI # q` are
the same two functions, there for languages in which a `{` in a string is
awkward
([`csi_hash_table`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/VTPrsTbl.c#L2566-L2583)).

With no parameters, a push saves all of the attributes. With parameters,
named by their SGR numbers, it saves only those, and the matching pop
restores only those and leaves the rest as they are:

| `Pm` | Attribute |
| --- | --- |
| `1` | bold |
| `2` | faint |
| `3` | italic |
| `4` | underline |
| `5` | blink |
| `7` | inverse |
| `8` | invisible |
| `9` | crossed out |
| `21` | double underline |
| `30` | the foreground color |
| `31` | the background color |

A color has many SGR numbers, so `30` and `31` stand for all of them; `10`
and `11`, which older xterms used for the colors, are still taken. Any other
number is ignored. A pop with nothing pushed does nothing.

The stack is ten deep. xterm counts pushes past the tenth without saving
anything for them, and the pops that match them restore nothing, so pushes
and pops still pair up: after eleven pushes, the first pop restores nothing
and the second restores what the tenth push saved
([`xtermPushSGR`, `xtermPopSGR`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/misc.c#L8120-L8282)).
Nothing resets the stack: neither [RIS](https://control-codes.page/esc/ris/index.html.md) nor
[DECSTR](https://control-codes.page/csi/decstr/index.html.md) touches it, so what was pushed before a reset can be
popped after it. These are xterm's own, and in a default build
([`OPT_XTERM_SGR`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/ptyx.h#L868-L870)).
xterm also has XTREPORTSGR, `CSI Pt ; Pl ; Pb ; Pr # |`, which reports the
attributes that every cell in a rectangle shares.

**Contour's SGRSAVE and SGRRESTORE.** Contour's
[Save and Restore SGR attributes](https://github.com/contour-terminal/contour/blob/d5541aa85daa1b5df77e90eb91f159cc91f12a61/docs/vt-extensions/save-and-restore-sgr-attributes.md),
read at commit `d5541aa`, gives `CSI # {` and `CSI # }` other names and a
simpler meaning: one saved set of attributes rather than a stack, and no
parameters. It says that pushing and popping some attributes and not
others is functionality a terminal need not have and a program can manage
for itself. In Contour at release 0.8.0, `CSI # {` saves all of the
attributes over whatever was saved before, `CSI # }` restores them as often
as it is sent, and a push with parameters is not recognized
([`Screen::saveGraphicsRendition`](https://github.com/contour-terminal/contour/blob/v0.8.0.9335/src/vtbackend/screen/Screen.cpp#L1343-L1351),
[`Functions.hpp`](https://github.com/contour-terminal/contour/blob/v0.8.0.9335/src/vtbackend/vt/Functions.hpp#L823-L824)).
A program that pushes once and pops once gets the same from both; one that
nests gets the innermost attributes back each time from Contour.

libghostty-vt recognizes neither function and ignores both, so the cases
in which a pop changes anything are known differences.

## Support

| Terminal | Version | Support | Notes |
| --- | --- | --- | --- |
| VT220 | [manual](https://vt100.net/docs/vt220-rm/), 2nd ed., 1984 | No | not in the manual |
| VT510 | [manual](https://vt100.net/docs/vt510-rm/), 1st ed., 1993 | No | not in the manual |
| xterm | patch 412, [xterm-411a](https://github.com/ThomasDickey/xterm-snapshots/tree/xterm-411a) | Yes |  |
| libghostty-vt | [0f171f6](https://github.com/ghostty-org/ghostty/tree/0f171f650a1cbbd6df4a2573b6b0c7de57cc6d90) | No | XTPUSHSGR-1 to XTPUSHSGR-3, XTPUSHSGR-5 to XTPUSHSGR-7: ignores both |
| kitty | 0.49.2, its documentation | No | not in its CSI dispatch; `CSI # P` and `CSI # Q` push and pop colors instead ([`vt-parser.c`](https://github.com/kovidgoyal/kitty/blob/v0.49.2/kitty/vt-parser.c#L1291-L1298)) |
| VTE | 0.84.1, [source](https://gitlab.gnome.org/GNOME/vte/-/tree/0.84.1) | No | recognized and ignored ([`vteseq.cc`](https://github.com/GNOME/vte/blob/0.84.1/src/vteseq.cc#L9657-L9674)) |

## Validation

### XTPUSHSGR-1: Pop brings everything back

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e[1;31m
\e[#{
\e[0;4m
A
\e[#}
B
```

Expected screen:

```text
|AB__|
attr 1,1 underline -bold fg=default
attr 1,2 bold -underline fg=1
```

### XTPUSHSGR-2: Only the attributes named

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e[1;31m
\e[1#{     # save bold only
\e[0m
A
\e[#}
B
```

Expected screen:

```text
|AB__|
attr 1,1 plain
attr 1,2 bold fg=default
```

### XTPUSHSGR-3: Pushes nest

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e[1m
\e[#{
\e[3m
\e[#{
\e[0m
A
\e[#}
B
\e[#}
C
```

Expected screen:

```text
|ABC_|
attr 1,1 plain
attr 1,2 bold italic
attr 1,3 bold -italic
```

### XTPUSHSGR-4: A pop with nothing pushed does nothing

Input, one step per line:

```text
\e[1m
\e[#}
A
```

Expected screen:

```text
|A___|
attr 1,1 bold
```

### XTPUSHSGR-5: The other final bytes

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e[1m
\e[#p
\e[0m
A
\e[#q
B
```

Expected screen:

```text
|AB__|
attr 1,1 plain
attr 1,2 bold
```

### XTPUSHSGR-6: Past ten, a push saves nothing

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e[1m
\e[#{\e[#{\e[#{\e[#{\e[#{\e[#{\e[#{\e[#{\e[#{\e[#{   # ten, all bold
\e[0;3m
\e[#{      # the eleventh is not saved
\e[0m
\e[#}      # so its pop restores nothing
A
\e[#}      # and the next brings back the tenth
B
```

Expected screen:

```text
|AB__|
attr 1,1 plain
attr 1,2 bold -italic
```

### XTPUSHSGR-7: RIS leaves the stack alone

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e[1m
\e[#{
\ec
\e[#}
A
```

Expected screen:

```text
|A___|
attr 1,1 bold
```

---

This is the Markdown version of <https://control-codes.page/csi/xtpushsgr/>. On that page every validation case runs live in libghostty-vt, the terminal emulation core of Ghostty, compiled to WebAssembly.

The validation cases are written in the notation described in <https://control-codes.page/notation/index.html.md>.
