Skip to content

Kitty keyboard protocol: main-screen flags are lost after entering and leaving the alternate screen #20763

Closed

Description

@Insulinocytus

Windows Terminal version

1.25.2733.0 (binary ProductVersion 1.25.260930003). Code inspection suggests v1.26.2734.0 and main are also affected; not tested at runtime.

Windows build number

10.0.26100.7462

Other Software

Found with oh-my-pi v18.6.0, a TUI that runs on the main screen with CSI > 1 u and switches to the alternate screen temporarily for fullscreen overlays. The repro below needs only PowerShell 7.

Steps to reproduce

Run this in a Windows Terminal PowerShell 7 tab (pwsh -NoProfile -File repro.ps1). It pushes Kitty flags on the main screen, enters and leaves the alternate screen without touching the Kitty stack, and queries the current flags (CSI ? u) after each step:

Add-Type -Namespace W -Name K -MemberDefinition @'
[DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int h);
[DllImport("kernel32.dll")] public static extern bool GetConsoleMode(IntPtr h, out uint m);
[DllImport("kernel32.dll")] public static extern bool SetConsoleMode(IntPtr h, uint m);
'@
$h = [W.K]::GetStdHandle(-10); $mode = 0; [void][W.K]::GetConsoleMode($h, [ref]$mode)
[void][W.K]::SetConsoleMode($h, ($mode -bor 0x200) -band -bnot 0x7) # VT input on; line/echo/processed off
$e = [char]27
function Query-Flags {
    [Console]::Write("$e[?u")
    $s = ''; while (-not $s.EndsWith('u')) { $s += [Console]::ReadKey($true).KeyChar }
    $s.Substring(1)
}
try {
    $r = [ordered]@{}
    [Console]::Write("$e[>1u");                 $r['1. push 1 on main screen          '] = Query-Flags
    [Console]::Write("$e[?1049h");              $r['2. enter alternate screen         '] = Query-Flags
    [Console]::Write("$e[?1049l");              $r['3. leave alternate screen         '] = Query-Flags
} finally {
    [Console]::Write("$e[<u"); [void][W.K]::SetConsoleMode($h, $mode)
}
$r.GetEnumerator() | ForEach-Object { "$($_.Key) -> ESC$($_.Value)" }

The same thing happens with the sequence a well-behaved app sends: CSI > 1 u on main, then CSI ? 1049 h, CSI > 1 u, CSI < u, CSI ? 1049 l.

Expected Behavior

1. push 1 on main screen           -> ESC[?1u
2. enter alternate screen          -> ESC[?0u
3. leave alternate screen          -> ESC[?1u

Leaving the alternate screen should bring back the flags that were active on the main screen. The spec says:

The main and alternate screens in the terminal emulator must maintain their own, independent, keyboard mode stacks. This is so that a program that uses the alternate screen such as an editor, can change the keyboard mode in the alternate screen only, without affecting the mode in the main screen or even knowing what that mode is.

Actual Behavior

1. push 1 on main screen           -> ESC[?1u
2. enter alternate screen          -> ESC[?0u
3. leave alternate screen          -> ESC[?0u

After step 3 the main screen is back to legacy encoding, but the main-screen app still assumes the flags it pushed are active. In practice, Shift+Enter starts arriving as a plain \r (so it submits instead of inserting a newline) and Ctrl+Enter as \n. Nothing tells the app that its mode was dropped.

Probable cause, in terminalInput.cpp:

// PushKittyFlags
stack.push_back(_kittyFlags);   // saves the flags in effect *before* the push
_kittyFlags = flags;            // flags that are now active live only here

// UseAlternateScreenBuffer
_kittyFlags = 0;                // main screen's active flags are not saved anywhere

// UseMainScreenBuffer
_kittyFlags = _kittyMainStack.empty() ? 0 : _kittyMainStack.back();  // restores the *pre-push* value

The stack holds the values to restore on pop, not the flags currently in effect. So _kittyMainStack.back() is the main screen's state from before its last push, which is 0 in the repro. One way to fix this is to save the main screen's _kittyFlags in UseAlternateScreenBuffer and restore that value in UseMainScreenBuffer, for example with separate _kittyMainFlags / _kittyAltFlags.

Activity

  1. added
    Needs-TriageIt's a new issue that the core contributor team needs to triage at the next triage meeting
    Needs-Tag-FixDoesn't match tag requirements
    on Oct 4, 2026
  2. added a commit that references this issue on Oct 6, 2026
    b7752ed
  3. added 2 commits that reference this issue on Oct 26, 2026
    8abcb39
    c6046b1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs-Tag-FixDoesn't match tag requirementsNeeds-TriageIt's a new issue that the core contributor team needs to triage at the next triage meeting

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions