0

I'm writing a bash script that displays content to the screen and then use ANSI commands (or tput) to move the cursor and update the content that was already displayed.

The problem I have is that when the the output starts towards the lower part of the terminal, the output may cause the terminal to scroll - making all absolute positioning command miss the mark.

Is there a way to measure how many rows did my output cause the terminal to scroll by, so I can fix my cursor addresses?

Cyrus
  • 84,225
  • 14
  • 89
  • 153
Guss
  • 30,470
  • 17
  • 104
  • 128
  • This might help: [How to get the cursor position in bash?](https://stackoverflow.com/q/2575037/3776858) – Cyrus Feb 20 '23 at 16:06
  • I suspect that you will need to use relative cursor positioning rather than absolute positioning. A user may do a variety of things that would affect terminal window size and scrolling behavior. See this answer which describes some of the `tput` commands for cursor movement. https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux/20983251#20983251 – Richard Chambers Feb 20 '23 at 16:14
  • 1
    @Cyrus, I'm aware of CPR, though it is using absolute courdinates that remain the same after scrolling. – Guss Feb 20 '23 at 16:34
  • What if the initial position scrolls off the top of the screen entirely? If there's not enough scrollback buffer (eg. tmux with small history-limit) then the initial position no longer exists, so where should the cursor be moved? – jhnc Feb 20 '23 at 16:58
  • @jhnc that is indeed an issue - which I decided not to tackle. I allow the user to set the max number items they want to see, with a sane default that should fit most use cases, and if they choose more than can fit on their screen - its their problem. – Guss Feb 20 '23 at 16:59

2 Answers2

1

Instead of counting number of scrolled lines ... could counting the number of lines of output from the command may be of benefit?

For example, if the terminal has 20 lines (tput lines) and the command output 40 lines then you know the 'scroll' was 20 lines (+/- depending on how you count the follow-on console/command prompt).

NOTE: at the moment I'm drawing a blank on (if?) how to determine the current line in the window (eg, 10 lines of output start at line 18 in a 20-line window, so scroll would be 8); could you force a clear to insure output always starts on line 1 ('at the top')?

Per this Q&A: Count number of lines of output, one idea:

outcnt=$(command 2>&1 | tee /dev/stderr | wc -l)

Taking for a test drive:

$ outcnt=$(printf "%s %s\n" {1..10} 2>&1 | tee /dev/stderr | wc -l) 
1 2
3 4
5 6
7 8
9 10

$ typeset -p outcnt
declare -- outcnt="5"

This particular example will also count stderr in the count:

$ outcnt=$(non-existent command 2>&1 | tee /dev/stderr | wc -l) 
-bash: non-existent: command not found

$ typeset -p outcnt
declare -- outcnt="1"

If you don't want to count stderr lines then remove the 2>&1:

$ outcnt=$(non-existent command | tee /dev/stderr | wc -l)
-bash: non-existent: command not found

$ typeset -p outcnt
declare -- outcnt="0"

Example of counting stdout but not stderr:

$ outcnt=$( { echo "hello"; non-existent command; } | tee /dev/stderr | wc -l)
-bash: non-existent: command not found
hello

$ typeset -p outcnt
declare -- outcnt="1"

A variation on this approach, especially if you can't/don't want to use outcnt=$(...), would be to have tee redirect a copy of the output to a file and then wc -l the file.

markp-fuso
  • 28,790
  • 4
  • 16
  • 36
  • The problem with using `wc -l` or another way to count the number of new line characters in the output - which is what I was doing originally - is that some output contains more text than the width of the terminal and would overflow. So outputting "one line" might result in the terminal scrolling much more than 1 line. – Guss Feb 20 '23 at 18:31
  • then you drop down to `wc -c / (tput cols)` :-) I know, I know, I know ... you've also got non-printing characters in the output so then you need to look at counting just the printable characters, right? 'course this (huge mess) all goes out the window if the user resizes the window ... – markp-fuso Feb 20 '23 at 19:39
  • using `wc -c / $COLUMNS` is pretty naive, not only because of control characters. See my self-answer below for a more complete calculation. – Guss Feb 20 '23 at 20:08
  • 1
    I was being facetious ... need to include many more :-) :-) :-) :-) – markp-fuso Feb 20 '23 at 20:26
1

I've currently kind of given up on using absolute cursor positioning (or even "save_cursor"/"restore_cursor") due to the scrolling issue. Another option that I implemented in the past for similar setups but didn't do it here (yet) is to use curses.

For now I'm using relative cursor movements and counting the rows of output to calculate how many rows to move up to get to the beginning of the output.

Calculating the rows of output in case of text overflowing the width of the terminal may be a bit challenging, I'm using the following code for that:

local rows=0
for line in "${lines[@]}"; do
    local linewidth=$(tr -dc '[:print:]' <<<"$line" | wc -c)
    local outrows=$(( linewidth / $COLUMNS + $( [ $((linewidth % $COLUMNS)) -eq 0 ] && echo 0 || echo 1 ) ))
    rows=$(( rows + outrows ))
done

Notes:

  1. $COLUMNS is a bash specific global variable that contains the current width of the terminal - the same as you get from $(tput cols).
  2. Handling terminal size changes using trap code WINCH is left as an exercise for the future...

If there is a better way to handle scrolling, I would still love to see a better answer, that I can accept.

Guss
  • 30,470
  • 17
  • 104
  • 128