Control cursor position also at bottom of window

I have a slight problem controlling the cursor position in a Bash terminal window. I have a function ask a question and then wait for an answer which is either 'y' or 'n' or a carriage return. Whenever the user enters anything else it just erases the answer and waits for the next one. However, the script behaves differently when the cursor is on the bottom line of a window as compared to any other line.

#!/bin/bash

# -----
# function: askYesOrNo
# purpose : ask question passed as $1
# return  : 0 if answer was y or Y, 
#           1 otherwise
# -----
function askYesOrNo {
        printf "\e[1;31;47m$1 [Y|n]\e[0m "      # print $1 in color
        printf "\e[s"                           # save cursor x-pos

        while true ; do
                read answer
                if [ -z "$answer" ] ; then
                        return 0
                elif [ "$(tr -d "NnYy" <<< $answer)" != "$answer" ] ; then
                        break
                fi
                # restore cursor x-pos, erase rest of line
#               printf "\e[u\e[K"     
                # restore cursor x-pos, erase rest of line
                printf "\e[u\e[1A\e[K"      
        done
        if [ "${answer^}" == "Y" ]; then
                return 0
        fi
        return 1
}


for (( i = 100, max = 110; i <= max; i++ )) ; do
        printf " %d\n" $i
        if [ $i -eq $max ] ; then
                if askYesOrNo "Do you wish to continue?" ; then
                        (( max += 10 ))
                fi
        fi
done

The for-loop is the 'productive' part. It just puts out numbers in a column. Every 10 numbers it asks if the user wants to continue and uses the function askYesOrNo for that purpose.

The problem lies with the two lines in the function askYesOrNo (at the end of the while-loop) and when the user enters an answer that is not Y, y, N, n or Enter. The first line (now commented out) works fine when the cursor is not in the bottom line of the window. But if it is then the window scrolls up and the next answer appears in the next line.

The second version moves the cursor one line up and that's fine with the cursor at the bottom of the window but not if the question is asked elsewhere.

How can I get this to work in every situation?

Hi Ralph...

Some terminals don't always follow terminal escape codes to the _letter_.
You could try and force the line prompt, force the cursor to the correct position and force clearing after the line prompt.
An example of the cursor forcing, it is just as easy to manipulate this to write the prompt and clear the line

printf "\033[12;36fSome prompt: \n"

But how do I know the correct position? It keeps changing as the script proceeds.

Would a ( man console_codes )

ESC M     RI       Reverse linefeed.

help?

An example clearing the screen/window:

while true
do
        printf "\033[2J\033[H\033[12;30fSome prompt: "
        read -r text
done

EDIT:

You can omit the \033[H if you wish but some terminals will not clear correctly without it...

Cool manual page but no... the page still scrolled up and I ended up in the next line, rather than restarting in the same line.

I think I have to check whether the cursor is in the last line of the window or not and do the reverse line feed only in that case. Together with an answer that I found elsewhere on this forum I came up with this version of the function which apparently works now:

function askYesOrNo {
        printf "\e[1;31;47m$1 [Y|n]\e[0m "      # print $1 in color
        printf "\e[s"                           # save cursor x-pos
    
        while true ; do
                read answer
                if [ -z "$answer" ] ; then
                        return 0
                elif [ "$(tr -d "NnYy" <<< $answer)" != "$answer" ] ; then
                        break
                fi

                # How many lines in this window?
                LINES=$(tput lines)

                # find cursor y-position ( line number )
                printf "\e[6n" ; read -sd R POS 
                CURPOS=${POS#*[}; CUR_Y=${CURPOS%;*}
 
                if [ "$CUR_Y" -eq "$LINES" ] ; then
#                       printf "\e[u\e[1A\e[K" 
                        printf "\e[u\eM\e[K"
                else
                        printf "\e[u\e[K" 
                fi                          
        done
        if [ "${answer^}" == "Y" ]; then 
                return 0
        fi  
        return 1
}

Same idea here, but also using CSI sequence

      r   DECSTBM   Set scrolling region; parameters are top and bottom row.

to reduce that region to last line. Not yet the ultimate answer, but worth a try, mayhap.

function askYesOrNo     { read -sdR -p $'\e[6n' TMP
                          TMP="${TMP#??}"
                          [ "${TMP%;*}" -eq "$LINES" ] && printf "\e7\e[$LINES;120r\e8"
                          while true 
                            do  read -p$'\e[1;31;47m'"$1 [Y|n]"$'\e[0m \e[s' answer
                                answer=${answer:-Y}
                                answer=${answer^^}
                                [ "${answer//[YN]}" = "${answer}" ] || break 
                            done
                          printf "\e7\e[1;${LINES}r\e8"
                          [ "$answer" = "Y" ] && return 0
                          return 1
                        }

It still duplicates the question lines when the cursor is in the middle of the screen.

What do the lines printf "\e7\e[$LINES;120r\e8" and printf "\e7\e[1;${LINES}r\e8" accomplish? I suppose you set the value of LINES to the number of lines of the window - let's say 20. Then the first snippet would say "\e7\e[20;120r\e8" and the second printf "\e7\e[1;20\e8" and r means 'set top and bottom lines of window.'

Not with my linux lxterminal . But I admit it might benefit from some more tweaking.

man bash :

The 120 is just an arbitrary value way beyond the lower screen boundary. The construct sets the scroll region from last line seen to somewhere way down...

For some reason LINES is not set in a non-interactive shell - even if checkwinsize is on. (I'm using Bash 5.0 but noticed that back in Bash 4.4, Kali Linux (which is Debian 9) and Raspian, too:

$ echo $LINES
24
$ bash
$ echo $LINES
24
$ cat showLINES
#!/bin/bash

echo LINES=$LINES

$ shopt checkwinsize
checkwinsize       on
$ bash showLINES
LINES=
 $ 

Hence I use LINES=$(tput lines) in my script.

But this works:

$ . ./showLINES
LINES=24
$

Mind to share the necessity of askYesOrNo in a non-interactive shell?

Try to

export LINES

Hi Ralph...

Take a look at the two command below.
The first will give you your terminal size in lines, columns.
IF your terminal can do it, (xterm for example), the second will auto adjust it for you...

term_size=($( stty size ))
printf "%b" "\x1B[8;24;80t"

The 24 and 80 in the 'printf' statement can be anything to ALMOST the size of your desktop...

Interesting. What do the outer parentheses around $(stty size) accomplish? I know they are used to force execution in a subshell. But what are they doing here?

I would have gone for

lines=$(stty size)
lines=${lines% *}

The printf statement doesn't seem to accomplish anything over here. What is it supposed to do? Is that documented somewhere?

In terms of documentation I was looking at this and this.

I did quote early on in this thread that some terminals do not respond correctly to some terminal escape codes. Some of those escape codes will not work at all.

So in the first part the outside parentheses create an array in advanced shells like bash so therefore longhand:

Last login: Tue Feb  4 16:17:10 on ttys000
AMIGA:amiga~> term_size=($( stty size ))
AMIGA:amiga~> 
AMIGA:amiga~> printf "%b\n" "${term_size[0]}"
24
AMIGA:amiga~> 
AMIGA:amiga~> printf "%b\n" "${term_size[1]}"
80
AMIGA:amiga~> _

As for the second 'printf' line, changing the values 24 and 80 to say 30 and 120 will expand the terminal size on certain terminals, (xterm as an exmaple), to that size for the duration of that terminal session. Of course calling it again with 24 and 80 restores it back to the original.
IF and a big if, it doesn't work then many of those terminal commands in the URLs won't work either.

Array... right. I wasn't thinking straight. Was early in the morning then.
Yes, probably quite a few of these escape codes won't work everywhere. Fortunately I don't need to resize the terminal. All I want is to keep the cursor in place until an acceptable answer arrives.
What about those code in man console_codes? Can't those be used to program in a reasonably safe / portable way in Bash?

Hi Ralph..
Is this what you are trying to do?

#!/bin/bash
# Linux Mint 19, default bash terminal.
clear
echo ""
echo ""
printf "%b" "This is a test line.\033[s\n"
sleep 2
echo ""
echo ""
printf "%b" "\033[u\033[1A\n"
echo "We are here.         "

This is what I do - the output of my script:

 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
Do you wish to continue? [Y|n] z

When a user enters an answer other than Y, y, N, n or <Enter> - for example 'z' above - then the cursor is supposed to stay where it is until an acceptable answer comes.
That has to work everywhere on the screen.

The problem I had was that the carriage return moved the cursor to the next line, one line below the question. I first fixed it for everywhere except the last line of the window and that's when I posted my original question.

This version works for me now:

#!/bin/bash

function askYesOrNo {
        printf "\e[1;31;47m$1 [Y|n]\e[0m "      # print $1 in color
        printf "\e[s"                           # save cursor x-pos

        while true ; do
                read answer
                if [ -z "$answer" ] ; then
                        return 0
                elif [ "$(tr -d "NnYy" <<< $answer)" != "$answer" ] ; then
                        break
                fi

                # How many lines in this window?
                LINES=$(tput lines)

                # find cursor y-position ( line number )
                printf "\e[6n" ; read -sd R POS
                CURPOS=${POS#*[}; CUR_Y=${CURPOS%;*}

                if [ "$CUR_Y" -eq "$LINES" ] ; then
#                       printf "\e[u\e[1A\e[K" 
                        printf "\e[u\eM\e[K"
                else
                        printf "\e[u\e[K"
                fi
        done
        if [ "${answer^}" == "Y" ]; then
                return 0
        fi
        return 1
}

for (( i = 100, max = 110; i <= max; i++ )) ; do
        printf " %d\n" $i
        if [ $i -eq $max ] ; then
                if askYesOrNo "Do you wish to continue?" ; then
                        (( max += 10 ))
                fi
        fi
done
 

Could be condensed a bit but this is easier to read.
It works here. Does it work on your system?

I think the carriage return at the end of the input is scrolling up the terminal. How about prompting for a single Y or N response (no need to CR):

#!/bin/bash
  
# -----
# function: askYesOrNo
# purpose : ask question passed as $1
# return  : 0 if answer was y or Y,
#           1 otherwise
# -----
function askYesOrNo {
        printf "\e[s"                           # save cursor x-pos

        while true ; do
                printf "\e[1;31;47m$1 [Y|n]\e[0m "      # print $1 in color
                read -n 1 answer
                case $answer in
                   Y|y|N|n) break;;
                esac
                # restore cursor x-pos
               printf "\e[u"
        done
        # restore cursor x-pos, erase rest of line
        printf "\e[u\e[K"
        # set return status
        [ "${answer^}" == "Y" ]
}


for (( i = 100, max = 110; i <= max; i++ )) ; do
        printf " %d\n" $i
        if [ $i -eq $max ] ; then
                if askYesOrNo "Do you wish to continue?" ; then
                        (( max += 10 ))
                fi
        fi
done

Yes, read -n 1 - I did that and it works but I wanted to have it my way. :smiley:

And my final version finally does what I want. :cool:

Mind to share your final version here?