How to start XTerm with prompt at the bottom?

5,034

Solution 1

If using bash, the following should do the trick:

TOLASTLINE=$(tput cup "$LINES")
PS1="\[$TOLASTLINE\]$PS1"

Or (less efficient as it runs one tput command before each prompt, but works after the terminal window has been resized):

PS1='\[$(tput cup "$LINES")\]'$PS1

To prevent tput from changing the exit code, you can explicitly save and reset it:

PS1='\[$(retval=$?;tput cup "$LINES";exit $retval)\]'$PS1

Note that the variable retval is local; it doesn't affect any retval variable you might have defined otherwise in the shell.

Since most terminals cup capability is the same \e[y;xH, you could also hardcode it:

PS1='\[\e[$LINES;1H\]'$PS1

If you want it to be safe against later resetting of PS1, you can also utilize the PROMPT_COMMAND variable. If set, it is run as command before the prompt is output. So the effect can also be achieved by

PROMPT_COMMAND='(retval=$?;tput cup "$LINES";exit $retval)'

Of course, while resetting PS1 won't affect this, some other software might also change PROMPT_COMMAND.

Solution 2

I have been using @Thomas Dickey's solution of including \[\033[1000H\] in PS1 for a while and it has been frustrating me ever since as it breaks vi mode editing on multi-line commands, which is something that comes up quite often.

After investigating the problem several times over the past few months, I finally found a trivial change that prevents the issue: instead of including the ANSI escape sequence in PS1, it should be directly printed to stdout from PROMPT_COMMAND (in ~/.bashrc or another configuration file):

PROMPT_COMMAND=prompt

prompt() {
    r="${?}"
    printf '\033[1000H'
    return "${r}"
}

printf and return are both builtins (see builtins(1)), so no processes are spawned. The exit status of the previous command is saved in r, cleared with printf, and restored with return. If PROMPT_COMMAND is already being used, this can simply be added to it.

This solution works with multi-line commands, but it has two minor limitations: first, clearing the screen with the clear-screen readline command (bound to Control-L by default) causes the prompt to go to the top of the screen, and second, increasing the size of a new terminal leaves the prompt where it was. In both cases, once the prompt is printed (e.g. by pressing enter) it goes back to the bottom of the screen (when bash runs PROMPT_COMMAND), which is an improvement over a single tput cup 1000 or similar at the start of ~/.bashrc as suggested by @phil pirozhkov.

Further investigation shows that the clear program works correctly because it prints terminal escape sequences directly (observable with clear | xxd or similar), which forces Bash to run PROMPT_COMMAND again. The clear-screen readline command, on the other hand, allows readline (used by Bash) to intercept the clear screen request and to simply re-display the previous prompt without rerunning PROMPT_COMMAND. Since the \033[1000H escape sequence is no longer in the prompt itself and rather is a side effect of running PROMPT_COMMAND, the prompt stays where it is.

The most general solution would be to patch Bash to run PROMPT_COMMAND when the terminal is resized or clear-screen is run. However, I opted for a more direct solution: patch the clear-screen command to print \033[1000H to stdout directly. For clarification, Bash's default source configuration includes its own copy of readline, but on my system (Gentoo Linux) the default Bash configuration is to use the system readline, and patching the system readline works for any other programs using it as well.

In any case, the simplest solution is to download Bash and apply the following patch with patch -p1 in the Bash source directory:

--- a/lib/readline/display.c    2021-12-20 10:00:33.370809888 +0000
+++ b/lib/readline/display.c    2021-12-20 09:59:14.920808045 +0000
@@ -3186,11 +3186,17 @@
   ScreenClear ();
   ScreenSetCursor (0, 0);
 #else
+  static char const cup[]={'\033', '[', '1', '0', '0', '0', 'H'};
+  size_t i;
+
   if (_rl_term_clrpag)
     {
       tputs (_rl_term_clrpag, 1, _rl_output_character_function);
       if (clrscr && _rl_term_clrscroll)
    tputs (_rl_term_clrscroll, 1, _rl_output_character_function);
+
+      for (i=0; i < sizeof(cup); i++)
+   putc (cup[i], rl_outstream);
     }
   else
     rl_crlf ();

Note that this patch works for readline 8.1, included with Bash 5.1. The patch does not apply to readline 8.0 because it does not include the clrscr parameter to the _rl_clear_screen function, but changing the patch for this is straight forward.

While I was at it, I decided to make another improvement: instead of deleting the screen contents with the clear-screen command, I wanted to scroll the contents up so that I could access them later in my terminal's scroll back buffer. In order to achieve this, the following patch can be applied on top of the previous one:

--- a/lib/readline/display.c    2021-12-20 23:48:23.253322032 +0000
+++ b/lib/readline/display.c    2021-12-20 23:48:41.813321757 +0000
@@ -3191,6 +3191,15 @@
 
   if (_rl_term_clrpag)
     {
+      if (!clrscr)
+      {
+   for (i=0; i < sizeof(cup); i++)
+     putc (cup[i], rl_outstream);
+
+   for (i=0; i < _rl_screenheight; i++)
+     putc ('\n', rl_outstream);
+      }
+
       tputs (_rl_term_clrpag, 1, _rl_output_character_function);
       if (clrscr && _rl_term_clrscroll)
    tputs (_rl_term_clrscroll, 1, _rl_output_character_function);

With this the terminal and shell finally work exactly as I want them to. The only remaining problem is that increasing the size of the terminal before you have entered the first command leaves the prompt where it is, until you enter the first command. However, this is essentially a non-issue. I wrote both of the patches here and I hereby release them into the public domain under the CC0 Public Domain Disclosure (in case someone would like to incorporate them into future projects).


Edit: As requested by @Toby Speight, this can be adapted to other terminals. For PROMPT_COMMAND, simply save the output of tput cup 1000 in a variable in ~/.bashrc and print it instead of \033[1000H, as is done in @Thomas Dickey's answer.

For the patches, I directly embedded the \033[1000H escape sequence for speed (so as to only print one escape sequence to adjust the cursor position) and because only I was using it. A more portable solution is easily possible with the other readline functions in the same file (display.c), specifically the _rl_move_vert function. Unfortunately this is complicated by the fact that Bash/readline does not seem to accurately know its current vertical cursor position (stored in _rl_last_v_pos) in most cases.

As far as I can tell the current cursor position is not queried upon startup, meaning it doesn't know where on the screen the prompt is starting. The _rl_move_vert function itself moves down with simply putc ('\n', rl_outstream), meaning that if it "moves down" when the cursor is already on the last line of the terminal it will actually scroll the terminal buffer.

The _rl_clear_screen function also doesn't update _rl_last_v_pos at all, meaning it doesn't know where the cursor is after the screen is cleared with the clear-screen command, however this can be fixed (presumably) by setting it to zero in _rl_clear_screen. readline could presumably be extended to use escape sequences to move the cursor position, similar to its select use of _rl_term_clrpag (to clear the screen) and other related escape sequences. However, I'm unsure of how exactly this works and where these escape sequences are initialized.

In any case, I wrote a new patch using the _rl_move_vert function that still works. It's different from the previous patch in that the scroll back saving doesn't always scroll a full page, and instead only scrolls as much as it needs to (which may be desirable) -- at least, I think that's what it does:

--- a/lib/readline/display.c    2021-12-21 09:57:55.932774006 +0000
+++ b/lib/readline/display.c    2021-12-21 09:19:59.109474783 +0000
@@ -3186,11 +3186,25 @@
   ScreenClear ();
   ScreenSetCursor (0, 0);
 #else
+  int i;
+
   if (_rl_term_clrpag)
     {
+      if (!clrscr)
+      {
+   i = _rl_screenheight-_rl_last_v_pos;
+   _rl_move_vert(_rl_screenheight);
+
+   for (; i < _rl_screenheight; i++)
+     putc ('\n', rl_outstream);
+      }
+
       tputs (_rl_term_clrpag, 1, _rl_output_character_function);
       if (clrscr && _rl_term_clrscroll)
    tputs (_rl_term_clrscroll, 1, _rl_output_character_function);
+
+      _rl_last_v_pos = 0;
+      _rl_move_vert(_rl_screenheight);
     }
   else
     rl_crlf ();

Solution 3

The answers using $LINES are unnecessarily non-portable. As done in resize, you can simply ask xterm to set the position to an arbitrarily large line number, e.g.,

tput cup 9999 0

(assuming that you have a window smaller than 10 thousand lines, disregarding scrollback).

Because the string will not change as a side-effect of resizing the window, you can compute this once, and paste it into your prompt-string as a constant, e.g.,

TPUT_END=$(tput cup 9999 0)

and later

PS1="${TPUT_END} myprompt: "

according to your preferences.

As for other processes modifying PS1: you will have to recompute PS1 after those changes, to ensure that it looks as you want. But there's not enough detail in the question to point out where to make the changes.

And finally: the behavior for tab-completion doesn't mesh with this sort of change, due to bash's assumptions.

Share:
5,034
l0b0
Author by

l0b0

Author, The newline Guide to Bash Scripting (https://www.newline.co/courses/newline-guide-to-bash-scripting). Hobby (https://gitlab.com/victor-engmark) &amp; work software developer.

Updated on September 18, 2022

Comments

  • l0b0
    l0b0 almost 2 years

    When starting XTerm the prompt starts at the first line of the terminal. When running commands the prompt moves down until it reaches the bottom, and from then on it stays there (not even Shift-Page Down or the mouse can change this). Rather than have the start of the terminal lifetime be "special" the prompt should always be at the bottom of the terminal. Please note that I have a multi-line prompt.

    Of course, it should otherwise work as before (resizeable, scrollable, no unnecessary newlines in the output, and no output mysteriously disappearing), so PROMPT_COMMAND='echo;echo;...' or similar is not an option. The solution ideally should not be shell-specific.

    Edit: The current solution, while working in simple cases, has a few issues:

    • It's Bash specific. An ideal solution should be portable to other shells.
    • It fails if other processes modify PS1. One example is virtualenv, which adds (virtualenv) at the start of PS1, which then always disappears just above the fold.
    • Ctrl-l now removes the last page of history.

    Is there a way to avoid these issues, short of forking XTerm?

  • SHW
    SHW almost 10 years
    How tput differ from many echo commands ? (Asking out of curiosity)
  • Stéphane Chazelas
    Stéphane Chazelas almost 10 years
    @l0b0, probably doesn't deserve a separate answer. I hope celtschk won't mind I edited his/her answer.
  • l0b0
    l0b0 almost 10 years
    '\[$(tput cup "$LINES")\]' works beautifully. Thanks!
  • l0b0
    l0b0 almost 10 years
    There is an issue with tput: It seems to reset $exit_code. Fixed by using \[\e[$LINES;1H\].
  • celtschk
    celtschk almost 10 years
    @l0b0: I've now added a version using tput that preserves the exit code.
  • Eric Hodonsky
    Eric Hodonsky about 8 years
    This is not working when I put it in my .bashrc file... :/
  • celtschk
    celtschk almost 8 years
    @Relic: Maybe something else changes PS1 later. I've now added another alternative independent of PS1.
  • l0b0
    l0b0 almost 8 years
    What do you mean by "the behavior for tab-completion doesn't mesh with this sort of change"?
  • l0b0
    l0b0 almost 8 years
    I think you mean PS1="${TPUT_END} myprompt: ", or even PS1="${TPUT_END}${PS1}"
  • Marius
    Marius almost 8 years
    For the latter - right (typo from thinking of makefiles). For former, I have in mind that bash's command-editing relies on being able to reprint the line (with prompt), and that you can get some odd behavior when this combines with scrolling due to command-completion.
  • l0b0
    l0b0 over 2 years
    Wow, fantastic work! Would you be willing to submit your patches to the readline project? I'd be super stoked to have this as an actual built-in feature, even if it ends up being behind a feature flag.
  • user17549713
    user17549713 over 2 years
    I'm all for it, but they would have to be configured by user options from inputrc since the default top terminal should remain, and that's more work than I'm interested in at the moment. As they are released into the public domain anyone is welcome to do so, however, or to send a link to this answer to their mailing list. For the time being, downloading Bash and applying the patches to its builtin readline is still easy enough even for people who aren't experienced with it. Anyways, thanks for the appreciation!
  • l0b0
    l0b0 over 2 years
    Thanks for the feedback! I've suggested it to the readline maintainer directly, as I couldn't find any relevant mailing lists.
  • Toby Speight
    Toby Speight over 2 years
    Can this be made so that it adapts properly to $TERM, rather than assuming the whole world is a VT-220 (or XTerm, or whatever)? It's bloody irritating to get a screen filled with ^[ crap because someone assumed that all terminals are identical...
  • user17549713
    user17549713 over 2 years
    @TobySpeight At the risk of making an unnecessarily detailed answer even more unnecessarily detailed, I added a brand spanking new edit section hot off the shelves including a new version of the patch that will probably work for any terminal.