2

This has been annoying me for ages. Even though I use a modern terminal emulator and most CLI programs, such as less and vim, are able to use the alternate screen adequately (i.e. entering it on startup and leaving it on exit), some are reluctant to do so. Rogue programs include top, screen and dialog.

When they start, these programs clear the contents of the window (i.e. the last N lines where N is the height of the terminal window). When they exit, some (top) leave their last state visible, while others (screen) clear the screen again. In all cases, the last N lines of scrollback buffer have been overwritten.

I checked this on various machines over time, and in various terminal emulators, including xcfce4-terminal, urxvt and screen (the alternate screen of which being properly enabled, using :altscreen on). Hence I do not think it is a problem with my terminal, I rather believe it is the built-in behavior of these programs (at least unpatched, as they are distributed in Archlinux).

So my questions are:

  1. Why do these programs behave like this? I guess there are good reasons for it?
  2. How can I work around it? Currently, I am using dumb wrapper scripts like the one below, but maybe there is a cleaner way?

    # save this as: ~/bin/top
    
    if [ -t 1 ]; then  # only use alt screen if output is a terminal
        tput smcup  # toggle alt screen on
        /usr/bin/top "$@"
        tput rmcup  # toggle alt screen off
    else
        /usr/bin/top "$@"
    fi
    
  • With altscreen on in my ~/.screenrc I don't have such an issue. Have you considered updating the version of screen that's installed? Seems like this is more than one question, also. – Wildcard Aug 2 at 23:13
  • As I said, I have seen the same behavior for years and with several different terminal emulators, and, besides, as an Archlinux user, I am running recent versions of all programs. So it cannot be because of outdated programs, nor because of screen’s “inner” configuration specifically. – Maëlan Aug 3 at 0:04
  • 1
    So then it seems you have several questions mixed together, and you don't want to consider applying patches from upstream. Please edit your question to clarify your exact question. The "better way" in my opinion would be to install current versions of the programs giving you trouble, but it's pretty broad right now. – Wildcard Aug 3 at 0:06
  • 2
    1. Some people hate the alternate screen feature and not only they don't care to fix programs to work with it, but also go out of their way to break it ;-) 2. patch the corresponding programs. For 2., notice that the switch should also be performed when the program is stopped/continued (eg. with ^Z), not only upon starting/exiting. That's something your wrapper fails to do. – mosvy Aug 3 at 1:51
  • 1
    Instead of 2., you should also consider using alternatives (htop, whiptail, tmux). Notice that simple programs which are using ncurses in the regular way (initscr()/endwin()) don't have to do anything special; the ncurses library takes care of everything. – mosvy Aug 3 at 2:17
1

Instead of trying to wrap using a shell script, we can write a short C program, which allows us to switch back to the normal screen when the program is stopped:

#define _GNU_SOURCE 1

#include <stdbool.h>
#include <stdio.h>


#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/* Quick hack; todo: use terminfo instead */
#include <stdlib.h>
static void enter_alt_screen(void)
{
    system("tput smcup");
}
static void leave_alt_screen(void)
{
    system("tput rmcup");
}

int main(int argc, char **argv)
{
    if (argc < 2) {
        fprintf(stderr, "Usage: %s command args...", argv[0]);
        return 1;
    }

    if (!isatty(fileno(stdout))) {
        /* not a terminal; act normally */
        execvp(argv[1], argv+1);
    }

    enter_alt_screen();
    const pid_t child = fork();
    switch (child) {
    case -1:
        leave_alt_screen();
        perror("fork");
        return 1;
    case 0:
        /* child */
        execvp(argv[1], argv+1);
        leave_alt_screen();
        perror("exec");
        return 1;
    }

    int status;
    while (waitpid(child, &status, WCONTINUED) == child) {
        if (WIFSTOPPED(status)) {
            leave_alt_screen();
        } else if (WIFCONTINUED(status)) {
            enter_alt_screen();
        } else if (WIFSIGNALED(status)) {
            leave_alt_screen();
            signal(WTERMSIG(status), signal(SIGTERM, SIG_DFL));
            raise(WTERMSIG(status));
        } else if (WIFEXITED(status)) {
            leave_alt_screen();
            return WEXITSTATUS(status);
        }
    }
    return 1;
}
  • Great piece of C code. I just don’t get the WIFSTOPPED / WIFCONTINUED part. If I understand the man page correctly, this should trigger when the child is stopped/continued, yet when I try it with /bin/top, pause it with Ctrl+z and resume it with fg, this is not detected (the shell prompt shows in the alternate screen). I was only able to trigger it by adding the flag WUNTRACED to the function waitpid(), and by stopping/continuing the child process with kill -STOP/-CONT (but then there is no point, as this does not yield a shell prompt). I don’t really know what I am doing. – Maëlan Nov 10 at 20:47
  • Strange: it works for me with sleep, but not with top (which is /usr/bin/top here). I don't know why top doesn't change back when it's suspended or killed from the shell. – Toby Speight Nov 14 at 17:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.