48

I'm on a terminal that supports the alternate screen which is used by less, vim, etc. to restore the previous display after exiting. That's a nice feature, but it really breaks the --quit-if-one-screen switch of less since in that case less switches to the alternate screen, displays its data, figures out there is only one screen, and exits, taking the contents of the alternate screen with it.

The commonly suggested workaround is using the --no-init switch to avoid using the alternate screen altogether. However, this is somewhat ugly because I do want to use it in case less actually acts as a pager. Therefore I'm looking for a solution to use the alternate screen only if less doesn't terminate automatically.

I'll mostly use this as Git's pager, so a wrapper shell script that only runs less in case there is enough output would be fine, too. At least if there's no way to do it without one.

ThiefMaster
  • 2,337

4 Answers4

27

Since less 530 (released in December 2017), less --quit-if-one-screen does not switch to the alternate screen if it reads less than one screenful. So you won't have this problem if your version of less is recent enough.

In earlier versions, less has to decide whether to use the alternate screen when it starts. You can't defer that choice to when it terminates.

You could call less, let it use the alternate screen, and cat the content onto the primary screen if less terminates automatically. However I don't know of a way to detect automatic termination.

On the other hand, it isn't that difficult to call cat for short inputs and less for larger inputs, even preserving buffering so that you don't have to wait for the whole input to start seeing stuff in less (the buffer may be slightly larger — you won't see anything until you have at least one screenful of data — but not much more).

#!/bin/sh
n=3  # number of screen lines that should remain visible in addition to the content
lines=
newline='
'
case $LINES in
  ''|*[!0-9]*) exec less;;
esac
while [ $n -lt $LINES ] && IFS= read -r line; do
  lines="$lines$newline$line"
done
if [ $n -eq $LINES ]; then
  { printf %s "$lines"; exec cat; } | exec less
else
  printf %s "$lines"
fi

You might prefer to see the lines on the main screen as they come in, and switch to the alternate screen if the lines would cause scrolling.

#!/bin/sh
n=3  # number of screen lines that should remain visible in addition to the content
beginning=
newline='
'
# If we can't determine the terminal height, execute less directly
[ -n "$LINES" ] || LINES=$(tput lines) 2>/dev/null
case $LINES in
  ''|*[!0-9]*) exec less "$@";;
esac
# Read and display enough lines to fill most of the terminal
while [ $n -lt $LINES ] && IFS= read -r line; do
  beginning="$beginning$newline$line"
  printf '%s\n' -- "$line"
  n=$((n + 1))
done
# If the input is longer, run the pager
if [ $n -eq $LINES ]; then
  { printf %s "$beginning"; exec cat; } | exec less "$@"
fi
  • 5
    "Less has to decide whether to use the alternate screen when it starts. You can't defer that choice to when it terminates." - although it apparently doesn't do it, but couldn't it simply defer any output (such as terminal initialization commands or actual data) until it has received X lines. If stdin is exhausted while X<TERMINAL_LINES it would simply dump everything on stdout and exit, otherwise it would initialize the alternate screen and do what it's supposed to do – ThiefMaster Jan 09 '14 at 08:06
  • 1
    I ended up using a modified version of your first code example: https://gist.github.com/ThiefMaster/8331024 ($LINES was empty when called as git-pager and I think you forgot incrementing $n) – ThiefMaster Jan 09 '14 at 09:01
  • @ThiefMaster Thanks for the feedback. Note that you should put #!/bin/bash in your script since you're using bash-specific construct, as it is your script won't work on systems (such as Ubuntu) where /bin/sh is not bash. – Gilles 'SO- stop being evil' Jan 09 '14 at 17:57
  • 3
    I really liked this idea, and developed it a bit further (with more features): https://github.com/stefanheule/smartless – stefan Jun 30 '15 at 17:17
  • 1
    @ThiefMaster: less could also (but doesn't) have an optional way to quit where it prints the current contents of the screen after sending the un-init string. So you could have the benefit of the alternate screen not cluttering the scrollback, but still leave the relevant part of the man-page or whatever on the terminal after exiting. – Peter Cordes Jun 09 '17 at 08:14
  • This is now out of date, less has been fixed. See eigengrau82's answer below. – gib Sep 26 '18 at 23:28
  • Is your script of interest for those with less <530? Do I need cat if I have less >=530? – Timo May 19 '21 at 19:54
  • @Timo You don't need to do anything if you have less ≥530. – Gilles 'SO- stop being evil' May 19 '21 at 21:00
  • There's a strange behavior with --quit-if-one-screen that would be funny if it wasn't so rage-inducing. If it doesn't fit in the screen initially, less starts, for which it naturally uses alternate mode, and then I expand the terminal (which is a common and easy operation with a tmux pane), thus expanding the screen, making it fit in one screen, less just up and quits leaving me holding the bag with alternate screen having "helpfully" cleaned out even whatever portion was visible at the time. This is a rather big flaw in the design of --quit-if-one-screen... (tested in less 551) – Steven Lu Feb 24 '23 at 08:02
  • It may be safe to say that this is a bug in less. If less has opened up some content for me to consume using alternate screen, under no circumstances should it then be allowed to auto-quit without being commanded to do so. – Steven Lu Feb 24 '23 at 08:04
13

GNU less v. 530 incorporates the Fedora-patch referred to by @paul-antoine-arras and will no longer output the terminal initialization sequence when --quit-if-one-screen is used and the input fits on one screen.

eigengrau
  • 131
  • 12
    Homebrew users on Mac OS can get this behavior immediately by running brew install less, and making sure $LESS has F and omits X. – Ryan Patterson Mar 29 '18 at 21:25
  • This is my favorite answer. I immediately downloaded Less 5.3.0 from GNU and compiled it myself. Great hint! – iBug Sep 16 '18 at 14:36
5

For slow inputs, like git log -Gregex, do you want:

A) lines to appear on the main screen as they come in, then switch to the alternate screen once scrolling is needed (so the first $LINES of output will always appear in your scrollback); if so, go with the 2nd of Gilles's answers.

B) lines to appear on the alternate screen, but quit the alternate screen and print the lines to the main screen if scrolling turns out to be unnecessary (so no output will appear in your scrollback if scrolling was required); if so, use the script below:

It tees the input to a temporary file, then once less exits it cats the temporary file if it contains fewer lines than the screen height:

#!/bin/bash

# Needed so less doesn't prevent trap from working.
set -m
# Keeps this script alive when Ctrl+C is pressed in less,
# so we still cat and rm $TMPFILE afterwards.
trap '' EXIT

TXTFILE=$(mktemp 2>/dev/null || mktemp -t 'tmp')

tee "$TXTFILE" | LESS=-FR command less "$@"

[[ -n $LINES ]] || LINES=$(tput lines)
[[ -n $COLUMNS ]] || COLUMNS=$(tput cols)
# Wrap lines before counting, unless you pass --chop-long-lines to less
# (the perl regex strips ANSI escapes).
if (( $(perl -pe 's/\e\[?.*?[\@-~]//g' "$TXTFILE" | fold -w "$COLUMNS" | wc -l) < $LINES )); then
    cat "$TXTFILE"
fi

rm "$TXTFILE"

Use it with export PAGER='/path/to/script'. That should be enough to make git use it, unless you've already overridden core.pager.

For possible enhancements, see also my slightly more fleshed version of this script at: https://github.com/johnmellor/scripts/blob/master/bin/least

4

This has long been solved in Red Hat-based distros by modifying the behavior of the -F option in less source code: see this patch from the Fedora Project, whose first version dates back to 2008. The idea is simply to get the height of the terminal (i.e. the maximum number of lines that can be displayed at once) and to omit the initialization and deinitialization sequences when the file fits in one screen. Thus, no need for the -X option and -F can be used consistently whatever the file length.

parras
  • 83