Re: keypad(TRUE) mode not immediately restored after SIGTSTP/SIGCONT

Thomas Dickey <[email protected]>
Newsgroups gmane.comp.lib.ncurses.bugs
Message-ID <[email protected]>
On Mon, Aug 10, 2026 at 10:58:35PM +0200, Ferenc Wágner wrote:
> Hi,
> 
> Working with the dialog utility (version 1.3-20250116 in Debian trixie)
> I noticed something unexpected.  Simple reproduction:
> 1. Start `dialog --menu Test 15 40 4 1 One 2 Two 3 Three 4 Four`.
> 2. Press Ctrl-Z to background dialog, then run `fg` at the shell prompt.
> 3. Press Down to move to the next menu item.
> 4. Instead of advancing the selection, dialog immediately exits as if
>    Esc had been pressed.
> 
> I worked with an AI agent on the phenomenon and came up with a sensible
> bug report for ncurses.  I tested the provided minimal reproduction code
> and it works as expected.  I'm not familiar with the ncurses code base,
> but I found the reasoning below plausible, hope you find it helpful.  I
> cannot judge the suggested fix direction, so take it with a bigger grain
> of salt, but the described workaround worked for me.  I'm willing to
> provide further details if needed.

That's actually done via a call to endwin, which doesn't reset keypad.
I can see that would be an improvement.
 
> Regards,
> Feri.
> 
> ------------------------------------------------------------------------
> 
> ncurses version: 6.5+20250216 (libncursesw6 6.5+20250216-2, Debian 13 "trixie")
> Also traced against the current ncurses/master sources on
> https://github.com/mirror/ncurses (a read-only mirror), so this is not
> specific to the packaged snapshot.
> 
> TERM=xterm (also reproduces with xterm-256color).
> 
> Summary
> -------
> After a curses program using keypad(win, TRUE) is suspended with Ctrl-Z
> (SIGTSTP) and resumed with `fg` (SIGCONT), the terminal is left out of
> application keypad mode. The *first* special key pressed after resume
> (e.g. an arrow key) is misreported: getch() returns a bare KEY_ESCAPE
> (27), typically followed immediately by the escape sequence's trailing
> bytes read back as ordinary characters, instead of the expected KEY_*
> code. Every special key pressed *after* that first one is reported
> correctly again -- ncurses silently self-heals, but one keystroke too
> late. The screen itself is redrawn correctly on resume, so this is
> easy to miss unless you specifically press a special key right after
> resuming.
> 
> This is user-visible and disruptive in interactive full-screen tools:
> in dialog(1)-based menus, for instance, the resulting bare Escape is
> interpreted as Cancel, so the very first keypress after resuming a
> suspended dialog(1) session unexpectedly cancels/exits the program
> instead of moving the menu selection.
> 
> Root cause
> ----------
> Traced in the current sources (ncurses/tty/lib_tstp.c,
> ncurses/tinfo/lib_options.c, ncurses/base/lib_getch.c):
> 
> 1. On SIGTSTP, handle_SIGTSTP() (ncurses/tty/lib_tstp.c) calls
>    endwin() before actually stopping the process, so the terminal is
>    usable by the shell while the program is suspended.
> 
> 2. endwin() (ncurses/base/lib_endwin.c) calls reset_shell_mode()
>    (ncurses/tinfo/lib_ttyflags.c; the "reset_shell_mode" branch of
>    drv_mode() in ncurses/tinfo/tinfo_driver.c on term-driver builds
>    does the equivalent), which unconditionally calls
>    _nc_keypad(sp, FALSE):
> 
>        if (SP_PARM) {
>            _nc_keypad(SP_PARM, FALSE);
>            _nc_flush();
>        }
> 
>    This call is necessary and deliberate here: while the program is
>    suspended the shell owns the terminal, and the shell does not want
>    function keys delivered in application-keypad encoding, so
>    `keypad_local` (rmkx) genuinely must be sent every time. The bug is
>    not that this call happens, but what it happens to clobber as a
>    side effect (see step 3).
> 
> 3. _nc_keypad(sp, FALSE) (ncurses/tinfo/lib_options.c) is the single
>    function used both to physically toggle the terminal's keypad
>    transmission mode (by sending keypad_xmit/keypad_local) and to
>    record the last-requested state, as one and the same flag:
> 
>        if (flag) {
>            (void) NCURSES_PUTP2_FLUSH("keypad_xmit", keypad_xmit);
>        } else if (keypad_local) {
>            (void) NCURSES_PUTP2_FLUSH("keypad_local", keypad_local);
>        }
>        ...
>        sp->_keypad_on = flag;
> 
>    Sending rmkx to the terminal is correct and required (step 2), but
>    because it goes through the same _nc_keypad() call that also
>    updates sp->_keypad_on, it has the side effect of clobbering the
>    one persistent record that reset_prog_mode() (step 4) consults to
>    decide whether keypad_xmit needs to be re-sent on resume. A
>    separate, per-window flag recording the application's actual
>    intent does exist -- win->_use_keypad, set once by
>    keypad(win, TRUE) and otherwise unchanged by the suspend/resume
>    cycle (see point 5) -- but reset_prog_mode() cannot consult it: it
>    runs at the SCREEN level, before any specific window is back in
>    hand, and a SCREEN may have several windows with independently
>    different keypad settings, so there is no single per-window value
>    it could even validly substitute for sp->_keypad_on here. Only
>    _nc_wgetch()'s self-heal check (step 5) ever compares the two
>    flags to each other, and by then it is one keystroke too late.
> 
> 4. After SIGCONT, handle_SIGTSTP()'s resume path calls doupdate(),
>    whose own comment says: "This relies on the fact that doupdate()
>    will restore the program-mode tty state, and issue enter_ca_mode
>    if need be." That happens via reset_prog_mode(), which -- in the
>    "reset_prog_mode" branch of drv_mode() (tinfo_driver.c), mirrored
>    in ncurses/tinfo/lib_ttyflags.c -- does:
> 
>        if (sp->_keypad_on)
>            _nc_keypad(sp, TRUE);
> 
>    Since step 3 already cleared `_keypad_on`, this condition is now
>    false, so `keypad_xmit` (smkx) is not re-sent here.
> 
> 5. Why only the *first* subsequent key is affected: _nc_wgetch()
>    (ncurses/base/lib_getch.c) independently self-heals this, but only
>    as each read is serviced. At the very top of every call, before
>    anything is read:
> 
>        if (win->_use_keypad != sp->_keypad_on)
>            _nc_keypad(sp, win->_use_keypad);
> 
>    `win->_use_keypad` is still TRUE (set once at startup and never
>    touched by the suspend/resume path), while `sp->_keypad_on` is
>    FALSE (cleared in step 3), so the very first getch() call after
>    resume detects the mismatch and calls _nc_keypad(sp, TRUE), which
>    re-sends `keypad_xmit` and sets `_keypad_on` back to TRUE. But this
>    happens only once that first getch() call has already started
>    servicing the read -- the terminal had already transmitted the
>    user's pending keypress (typed right after `fg`) in *normal*
>    (non-application) mode, before this correction reaches it. So that
>    one keystroke is misparsed as a bare Escape plus literal trailing
>    bytes, using the stale terminal state; every keystroke after it
>    arrives once the terminal is correctly back in application mode
>    and is reported correctly.
> 
> Net effect: `_keypad_on` is being used for two different things --
> "should the terminal currently be sending function-key escape
> sequences" (a physical/shell-mode concern) and "did the application
> ask for keypad mode" (a program-mode concern, already tracked
> separately per-window as `_use_keypad`) -- and the shell-mode
> transition clobbers the flag the program-mode transition and the
> per-getch() self-heal both depend on, delaying the correction by
> exactly one keystroke.
> 
> This matches independently reported instances of the same symptom
> that predate this analysis:
>   - https://stackoverflow.com/questions/1189708 ("ncurses to external
>     shell and back messing with keys" -- "ncurses thinks ^[ and A are
>     seen respectively if I press the up arrow twice"; the accepted
>     answer's workaround is to explicitly call keypad(win, TRUE) again,
>     which works because keypad() calls _nc_keypad() unconditionally,
>     bypassing the stale flag)
>   - https://stackoverflow.com/questions/3328528 (same symptom pattern
>     after a def_prog_mode()/endwin()/.../reset_prog_mode()/refresh()
>     round trip)
> 
> How to reproduce
> -----------------
> Minimal, self-contained reproducer attached (repro.c),
> using nothing but ncurses' own default SIGTSTP/SIGCONT handling (no
> application-installed signal handler):
> 
>     cc -o repro repro.c -lncursesw
>     ./repro
> 
> Steps:
>   1. Press an arrow key a few times: getch() correctly reports
>      KEY_UP / KEY_DOWN / KEY_LEFT / KEY_RIGHT.
>   2. Press Ctrl-Z, then run `fg` in the shell to resume.
>   3. Press an arrow key once: getch() reports a bare KEY_ESCAPE (27)
>      instead of the expected KEY_* code (the sequence's trailing
>      bytes, e.g. 'O' and 'B' for xterm's Down arrow, are read back
>      immediately afterward as ordinary characters).
>   4. Press an arrow key again: getch() now correctly reports the
>      expected KEY_* code again, without any further Ctrl-Z/fg cycle.
> 
> Expected: arrow keys are recognized identically before and after a
> Ctrl-Z/fg cycle, with no one-keystroke "misfire" window.
> 
> Actual: exactly the first special key pressed after resume is
> misparsed as a bare Escape plus literal trailing bytes; every key
> after that is fine again.
> 
> Suggested fix
> -------------
> sp->_keypad_on is overloaded: reset_shell_mode() must unconditionally
> turn the terminal's *physical* keypad mode off for the shell's sake,
> but it does so via _nc_keypad(), which also happens to overwrite the
> one persistent record (sp->_keypad_on) that reset_prog_mode() later
> relies on to know whether keypad mode should be turned back on. A
> per-window record of the application's actual intent already exists
> (win->_use_keypad), but reset_prog_mode() runs before any window is
> back in hand and can't use it directly.
> 
> A fix along the following lines would let reset_prog_mode() do the
> right thing immediately, instead of leaving it to be discovered one
> keystroke late by _nc_wgetch()'s self-heal check: save
> sp->_keypad_on's value before reset_shell_mode() clears it (e.g. in
> def_prog_mode(), analogous to how it already saves other tty state
> across this same round trip via Nttyb), and restore it afterward, so
> that reset_prog_mode()'s existing
> `if (sp->_keypad_on) _nc_keypad(sp, TRUE);` check sees the correct
> pre-suspend value and can re-send keypad_xmit right away, before the
> user's next keystroke is even typed.
> 
> Workaround in the meantime, for applications that cannot patch
> ncurses: install a SIGCONT handler that unconditionally re-sends the
> terminfo `smkx` capability (or calls keypad(win, TRUE) again) as soon
> as the process resumes, before the user's next keystroke can reach the
> terminal in the wrong mode.
> 
> Happy to provide more detail, a pty-based automated reproduction
> script, or test a candidate patch.
> 
> Thank you for maintaining ncurses.
> 

> /*
>  * Minimal reproducer for: after suspending an ncurses program with
>  * Ctrl-Z (SIGTSTP) and resuming it with `fg` (SIGCONT), the terminal is
>  * left out of application keypad mode, so the next special key (e.g. an
>  * arrow key) is reported back as a bare KEY_ESCAPE followed by the
>  * literal trailing bytes of the escape sequence, instead of the
>  * expected KEY_* code.
>  *
>  * Build:   cc -o keypad_sigtstp_repro keypad_sigtstp_repro.c -lncursesw
>  * Run:     ./keypad_sigtstp_repro
>  * Steps:   1. Press an arrow key a few times -- observe correct KEY_UP /
>  *             KEY_DOWN / ... reports.
>  *          2. Press Ctrl-Z, then run `fg` to resume.
>  *          3. Press an arrow key again -- it is now reported as
>  *             KEY_ESCAPE (or an unrecognized raw byte sequence),
>  *             instead of the expected KEY_* code.
>  *
>  * This relies only on ncurses' own default SIGTSTP handling -- no
>  * application-level signal handler is installed.
>  */
> #include <curses.h>
> #include <stdlib.h>
> 
> int
> main(void)
> {
>     initscr();
>     cbreak();
>     noecho();
>     keypad(stdscr, TRUE);
> 
>     printw("Press arrow keys; Ctrl-Z then `fg` to reproduce; 'q' to quit.\n");
>     refresh();
> 
>     for (;;) {
>         int ch = getch();
>         if (ch == 'q')
>             break;
> 
>         printw("getch() returned %d", ch);
>         if (ch == KEY_UP)
>             printw(" (KEY_UP)");
>         else if (ch == KEY_DOWN)
>             printw(" (KEY_DOWN)");
>         else if (ch == KEY_LEFT)
>             printw(" (KEY_LEFT)");
>         else if (ch == KEY_RIGHT)
>             printw(" (KEY_RIGHT)");
>         else if (ch == 27)
>             printw(" (bare ESC -- bug reproduced if this follows Ctrl-Z/fg "
>                    "and an arrow key was pressed)");
>         printw("\n");
>         refresh();
>     }
> 
>     endwin();
>     return EXIT_SUCCESS;
> }


-- 
Thomas E. Dickey <[email protected]>
https://invisible-island.net
signature.asc (application/pgp-signature, 659 B)
-----BEGIN PGP SIGNATURE-----

iQGzBAABCgAdFiEEGYgtkt2kxADCLA1WzCr0RyFnvgMFAmp6X6sACgkQzCr0RyFn
vgMSdgwAr3bPnz23d/h7oaYFqZyVdfxdvjrwn4bLKI/PQXhLX3fzeeOFS614DxPt
yEL9XqD3wRczqEGKvdOESp7fPPh85Y6je2SIt2oTTJBgKTmWw1rakGDmZ0MXkHVM
yHkWuKKlX3G6RUxgk6zJN1NSH5jhDrG7ROgBmdhhtRAomwVe2ewhQ3NO1rI4FRTS
4gDBGzbP7+WG4psrG6ChPrXSgh+S1r5IBTT45jAJVF1dkDA5/PwHI9NZmZblVas5
5CDAkPd/jOdxg4eWlp04Jrs/8p8TV6OD2hiyqdN+NVuhxlNXIS9l4p9kvqm0v2v2
6UojCLlvgrYOdcwA2vTVj9mfhbgjgfoWIHK/onLGzgw2PTFtRfkREK0RQRlwGdUY
jmy81+rKqU67puIeaq4ess6+wUJFRRcRdDkmytGiQTBRnGM579tMKIltOJJwYZh/
lGPpo6pzOzAKf8xtUPreAglJDvYwGovqbBZkXEAWCK1L/zVE9KPfgDATFsaESQE/
iZ9Hbm4G
=9+lZ
-----END PGP SIGNATURE-----
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.