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 ();
clear
command.