I have the following BASH function that takes arguments and displays them at the bottom of the terminal in a new line that's excluded from the scroll region:
bottomLine() {
clear
# Get all arguments and assign them to a var
CONTENT=$#
# Save cursor position
tput sc
# Add a new line
tput il 1
# Change scroll region to exclude the last lines
tput csr 0 $(($(tput lines) - 3))
# Move cursor to bottom line
tput cup $(tput lines) 0
# Clear to the end of the line
tput el
# Echo the content on that row
echo -ne "${CONTENT}"
# Restore cursor position
tput rc
}
It's fairly straightforward and works. Thing is, after some commands (sometimes after just a few, sometimes after 15 minutes of work) the line would get scrolled up even though it should be excluded from the scrolling region.
This happens to me in both Tilda and Terminator.
Any help would be appreciated, cheers.
EDIT: The best way to reproduce the issue is if you do several "ls -a, ls -a, ls -a, ls -a" until you reach the bottom of the page, then open a random file with Vi and then do another "ls -a". When you do this, the unscrollable bottom row goes above even though it shouldn't.
My first impulse was to answer that there is no way to freeze the scrollable region once and forever, since any program manipulating the terminal (like vim does) can override your settings. However then I figured out that you can restore the settings through the shell prompting functionality. To this end you must add your terminal control sequence to the PS1 environment variable.
I've modified your function so that it automatically updates the prompt the first time it is called. To this end I had to split it into two functions.
bottomLineTermCtlSeq() {
#clear
# Save cursor position
tput sc
# Add a new line
tput il 1
# Change scroll region to exclude the last lines
tput csr 0 $(($(tput lines) - 3))
# Move cursor to bottom line
tput cup $(tput lines) 0
# Clear to the end of the line
tput el
# Echo the content on that row
cat "${BOTTOM_LINE_CONTENT_FILE}"
# Restore cursor position
tput rc
}
bottomLine() {
local bottomLinePromptSeq='\[$(bottomLineTermCtlSeq)\]'
if [[ "$PS1" != *$bottomLinePromptSeq* ]]
then
PS1="$bottomLinePromptSeq$PS1"
fi
if [ -z "$BOTTOM_LINE_CONTENT_FILE" ]
then
export BOTTOM_LINE_CONTENT_FILE="$(mktemp --tmpdir bottom_line.$$.XXX)"
fi
echo -ne "$#" > "$BOTTOM_LINE_CONTENT_FILE"
bottomLineTermCtlSeq
}
I store the current content of the bottom line in a file rather than in an environment variable so that subprocesses of the top level shell can also manipulate the bottom line.
Note that I removed the clear command from the terminal manipulation sequence, which means that you may need to call it yourself before calling bottomLine for the first time (when you call it while having reached to the bottom of your screen).
Also note that the bottom line can be messed up when the terminal window is resized.
Related
I have code :
printf '\r%s' 'first line here' 'second line here' 'thrid line heere'
sleep 1
printf '\r%s' 'iam new' 'new again' 'and new'
but the script show only one line
i want show three line, each line will update after one second, without new line again
only three line and update all three line
thanks
Assuming the objective is to print 3 lines, wait a bit, and then overwrite the same 3 lines, then you'll need to incorporate some additional code that provides for 'up and down' movement of the cursor.
While you can hardcode some of this based on a specific terminal, an easier (most of the time) approach is to use something like tput to manage cursor movement.
In this case you'll need the ability to not only 'go up' with the cursor but also overwrite/clear a previous set of output in the case where the new output is shorter in length; we'll also need to replace the current \r with \n.
The general approach:
EraseToEOL=$(tput el) # grab code for clearing from cursor to end of line; useful for erasing text from a previous longer print
tput sc # save the point we want to return to
printf "%s\n" 'first line here' 'second line here' 'third line here'
sleep 1
tput rc # return to our save point (tput sc), ie, the place where the cursor was located just before the 1st printf
printf "%s${EraseToEOL}\n" 'i am new' 'new again' 'and new'
The results:
FWIW, here's what it looks like if we remove ${EraseToEOL} from the 2nd printf:
tl;dr :
printf "One Line\nSecond line\nThird line"
sleep 1
printf "\033[1F\033[1Fnew 1\nnew 2\nnew 3"
Not sure what you are trying to do exactly.
But I am under the impression that you want to update three different lines.
Eg, you want your output to go from
first line here
second line here
third line heere (sic)
to
iam new
new again
and new
If so, you can't do that with \n, \r...
As Paul 's explained, those are inherited from the days when there was no screen but printers. And not modern printers: some electronically controlled good old typing machines.
So sending 'A' to the machine, types 'A'.
Etc. That the ascii code.
And some special codes meant special behaviour.
Such as '\r' which return to the beginning of the line (as it is possible with a typing machine, as you know if you've ever seen one). Or '\n', going to next line (using the right handle). Or '\07' ringing the bell. Etc.
But you cannot go back 1 line. Well, not with basic control char.
Now, depending on what terminal (the application emulating a physical terminal such as VT100, which itself is a device emulating a paperless printer. The "size of train is derived from size of roman horse ass" joke, is not entirely a myth) you are using, you may use special control sequences.
The most likely to work is ESC [ 1 F one.
So, in bash
printf "One Line\nSecond line\nThird line"
sleep 1
printf "\033[1F\033[1Fnew 1\nnew 2\nnew 3"
You may need to print some spaces to erase the remaining of the former line (or use some other control sequence to do so. You could check vt100 control sequences to find many others)
For example
printf "\n\n" # just to "create" the 2 extra lines
n=1
while true
do
printf "\033[1F\033[1F"
printf "n = $n\n"
printf "n² = $((n*n))\n"
printf "1000000/n = $((1000000/n)) " # <- note the trailing spaces because this line may become shorter and shorter
((n++))
sleep 1
done
(\033 is ESCape character. So each \033[1F sequence take you to the beginning of the previous line)
It is important tho to understand that you are assuming, doing so, that the terminal you are using understand those sequence. \n and \r are supposed to work with any device that understands ascii control char. VT100 Escape sequence are only working with devices that were designed to be compatible with those sequences. Which is the case of most terminal, but not all. A notable exception is jupyter notebook for example (And probably windows command terminal, but I don't have one to check).
So, if you need a clean solution for such things, you need to find a library such as bashsimplecurses.
That will adapt to any terminal.
Carriage returns (\r) ONLY reset to the beginning of the line.
Newline characters (\n) "roll the paper" a line vertically.
Imagine controlling a physical print head.
The result is that you can use \r for dynamic output effects like a countdown.
printf "\n" # get a clean line to start so you don't print over something
for c in {10..1}; do printf "\rNew lines in: %2.2s" $c; sleep 1; done # countdown in place
printf "\r%20.20s\r" "" # blank the line and reset to overwrite cleanly
printf "New line %2.2s!\n" {1..10}; # include \n's to move to next line, NOT overwrite
This counts down in place, doesn't leave the hanging 0 from the 10 (that's what the %2.2s if for), and writes the first of the lines that stay over where the countdown was without leaving hanging characters there (that's the %20.20s).
edit
I think I understand the OP a little better today.
The main point was to use printf '%s\n' instead of printf '\r%s'.
Others have done an excellent job of demonstrating how to move the cursor back up the screen to a previous point, but I wanted to throw one more perspective.
IF it's ok for this stuff to be the ONLY thing on the screen, there's a simple solution without quite so much explicit vertical cursor management -
clear # blank the screen, putting the cursor at the top
printf '%s\n' 'first line here' 'second line here' 'third line here'
sleep 1
clear # blank the screen and put the cursor at the top again
printf '%s\n' 'I am new' 'new again' 'and new'
Sometimes simple is easier to understand, use, and remember.
The other solutions here are a lot more flexible, though.
I recommend learning those.
I am calling it an ASCII animation for lack of a better word. What I am referring to is for example a loading bar, let's say like in pacman (arch package manager) ,that starts like this...
[ ]
and turns over time to this...
[#### ]
From my understanding of stdout I can't seem to wrap my head around this seemingly simple feature.I expect...
[ ]
[# ]
[### ]
...
What I'm not understanding is how is it able to print on top of stdout ,(if it's even doing that).
We sometimes think of terminals as just showing text, but they're actually more like browsers, rendering their own little markup in the form of control characters and ANSI terminal escape codes.
Simple, single-line animations are typically done using the Carriage Return control character. By writing a carriage return, the cursor returns to the left-most margin, and you can therefore write over the line again as many times as you'd like.
You'd obviously use a loop but here's an example written out for clarity:
{
printf '[## ]'
sleep 1
printf '\r[### ]'
sleep 1
printf '\r[#### ]'
}
For more advanced animations, you can e.g. arbitrarily position the cursor by writing special ANSI escape sequences as text. The tput tool is helpful for this in shell scripts, and tput cup 4 50 will output an ANSI sequence to move the cursor to line 4 column 50. This is equivalent to printf '\x1B[4;50H' and just writes a snippet of magic text to the terminal.
Here's this functionality used for a starfield animation (ctrl-c to exit):
while sleep 0.1
do
tput cup $((RANDOM%LINES)) $((RANDOM%COLUMNS))
printf "*"
done
Even tools like top and nano show what they show by carefully writing text and control characters to create color, lines, refreshing lists, etc.
I have this simple zshrc which displays time in prommpt and resets it every 1 second
below is simplified version of my zshrc
repeat_string(){
# this works fine
printf "-%.0s" $(seq 1 $(( $COLUMNS - 1)))
# this doesn't works fine
# printf "-%.0s" $(seq 1 $COLUMNS)
}
TMOUT=1
TRAPALRM() {
PROMPT="$(repeat_string)
$(date)
hello >>>"
zle reset-prompt
}
I have simple function here repeat_string which I'm calling in my prompt string. purpose of this function is to display seperator (-) which has length equal to column width. It works fine when I pass repeat count which is not equal to $COLUMN. but if I pass $COLUMN, it behaves weird and truncates previous prompts and stdout also. Here is asciicinema link. https://asciinema.org/a/9FhIvtLD0XTnctEUXSRyZ9IrC
Use following script to quickly reproduce issue
mkdir /tmp/zshdebug
cat <<'EOF' > /tmp/zshdebug/.zshrc
repeat_string(){
# this works fine
printf "-%.0s" $(seq 1 $(( $COLUMNS - 1)))
# this doesn't works fine
# printf "-%.0s" $(seq 1 $COLUMNS)
}
TMOUT=1
TRAPALRM() {
PROMPT="$(repeat_string)
$(date)
hello >>>"
zle reset-prompt
}
EOF
ZDOTDIR=/tmp/zshdebug zsh
zsh version: zsh 5.2 (x86_64-apple-darwin16.0)
Found solution here https://www.zsh.org/mla/users/2011/msg00052.html
This is a problem with terminals and terminfo or termcap descriptions.
Some terminals silently discard a newline when one is output at the
right margin, others don't, and still others discard it only when it
is output at the right margin of the very last line of the screen.
Some terminals cause the cursor to wrap onto the next line right
away when the right margin is reached, others leave the cursor on
top of the last character that was output until at least one more
character appears.
Terminal description databases aren't very good at differentiating
these cases. ZLE decides in this case that the terminal both wraps
the line and does not discard the newline, so it believes it needs
to move up one line before redrawing the prompt. Your terminal
wraps at the margin but does discard the newline, so ZLE's internal
idea of what the screen looks like does not match reality.
You can get around this by replacing $'\n' with $'%1(l.\n.)' which
means to emit the newline only if ZLE believes that at least one
character has been output since the last (implicit) newline. If
the line is exactly $COLUMNS wide, ZLE will think that the terminal
has wrapped and that zero characters have been output since, so it
won't fool itself by emitting a newline that the terminal discards.
On a different terminal, though, you might need to go back to $'\n'
unconditionally.
This question already has answers here:
Variable in Bash Script that keeps it value from the last time running
(3 answers)
Closed 7 years ago.
TL;DR: How do you set up a variable upon first run of a bash script and modify it upon further runs (something like a static variable in a function in C)?
Background info:
The following line in Bash is for playing "Command line Russian Roulette" (a much safer version of https://stackoverflow.com/a/575464/3696619):
[ $[ $RANDOM % 6 ] == 0 ] && echo *Boom* || echo *Click*
However, it doesn't really work like normal Russian Roulette (for which, within the first 6 tries, you are guaranteed to have a bullet in one of them).
Is there any way to make sure that this occurs (i.e. a *Boom* is guaranteed within the first 6 tries). The code should be repeatable (i.e. exact same code should be able to be copy pasted again and again, not parts of it) and it should work properly.
My idea is: Set up a variable that is set to $[$RANDOM % 6] initially and then decremented each time the code is run. If the variable reaches 0, the gun goes *Boom*, otherwise, it *Click*s. However, I am unable to figure out how to set up the variable only upon first run, and not on further runs.
Warning: Execute the code from https://stackoverflow.com/a/575464/3696619 only at your own risk.
Short answer: No, bash doesn't have "static" variables.
Longer: When your "C" program finishes its run, the "static" variable is lost. The same is applied for the bash script. So, you must decide what you want:
run the script once, simulate the revolver (and you can use global variable to hold the revolver status)
want preserve the revolver's cylinder status between each script run, so you need use some external storage to preserve the data. (for example file).
From you question i guessed the second version, so you must use file to hold the revolver status. For the game, you should divide the problem to different parts:
roll - (roll the revolver's cylinder (random bullet position))
shoot - (move the cylinder by one position and check the bullet)
I would do this as the following:
define the revolver for example as /tmp/revolver.
It will have 6 lines, in each line could be 2 values: 0 - no bullet, 1 - bullet
the hammer is in the 1st line - so, if the bullet is in the first line (e.g. the 1st line has value 1) the bullet will fire.
each "roll" ensures than exactly ONE bullet is in the cylinder
when shooting - once the bullet is fired, will not fire again, so any number of subsequent shots will not fire again
the roll "command". Defined as an bash function and saved as ./roll command.
revolver="/tmp/revolver"
roll() {
cyl=(0 0 0 0 0 0) # empty cylinder
cyl[$(($RANDOM % 6))]=1 # one bullet at random position
printf "%d\n" "${cyl[#]}" >"$revolver" # save
}
roll #do the roll
now, you can do bash roll (or after the chmod 755 roll) simply ./roll and your revolver is loaded with one bullet. Run the ./roll few times and after each ./roll check the bullet position with cat /tmp/revolver.
the shoot command should:
rotate the lines by one position (as in the real revolver)
and cock the hammer - e.g. check the value of the 1st line
revolver="/tmp/revolver"
rollone() {
at_hammer=$1 # store what is under the hammer
shift # shift the rest by one position
printf "%d\n" "$#" 0 > "$revolver" # save new cylinder the status
# note, we adding to the last position 0,
# because when the bullet is fired it will not fire again
# out digital revolver is not jamming
echo $at_hammer # return the bullet yes/no
}
shoot() {
cyl=($(<"$revolver")) #load the revolver status
return $(rollone "${cyl[#]}") #make the shoot, roll the cylinder and return the status
}
boom() { echo "Boom"; } #the "boom" action
click() { echo "Click"; } #the "click" action
shoot && click || boom #the actual shot
Now you can play the game:
a. ./roll - load the revolver with one bullet and roll the cylinder
b. ./shoot - any number of times
The whole game as script.
Variant A - roll once and shooting multiple times i
./roll
while :
do
./shoot
done
this will output something like:
Click
Click
Boom
Click
Click
... and forever ...
Click
Variant B - roll (e.g. reload with 1 bullet) between each shot
while :
do
./roll
./shoot
done
this will prints e.g.:
Click
Click
Boom
Boom
Click
Click
Click
Boom
Click
Click
Click
Click
Click
Click
Click
Click
Click
Boom
Click
... etc ...
Also, you could extend/modify the scripts with one more command: load and redefine your revolver as:
load - will load one (or more) bullet(s) into the cylinder
roll - will rotate the cylinder by the random number of positions (but NOT reloads the bullet) - e.g. after the fired bullet the roll will rotate only empty cylinder
shoot - fire the gun (no modification needed).
I don't think there's a different way for persisting the value without using a file. So, if you're going to use a file you could have the command below:
f=/tmp/a; test -f $f || echo $(($RANDOM%6))>$f; test $(<$f) == "0" && echo *Boom* && rm $f; test -f $f && echo *Click* && echo $(($(<$f)-1))>$f
What actually happens is:
Declare a variable $f where to store the name of the file that we use to store the variable.
f=/tmp/a;
Generate a random number between 0 and 5 and store it in the file:
test -f $f || echo $(($RANDOM%6))>$f;
If what we have in the file is 0 then print Boom and remove the file:
test $(<$f) == "0" && echo *Boom* && rm $f;
If the file is still there, print Click and decrement the value by 1:
test -f $f && echo *Click* && echo $(($(<$f)-1))>$f
Is there anyway to modify the bash profile scripts to always display a colored bar at top of screen. I have a requirement to show a colored hostname, username, and ipaddress on the screen at all times, but i don't want to overload PS1 as it would make the prompt take up over half of the default console width.
Not perfect, but this shows you how to fix part of your prompt on the first row of the screen:
PS1='\w \[\e[s\e[1;1H\e[42m\]\h \u ipaddress\[\e[0m\e[u\]\$ '
A breakdown:
\e[s - save the current cursor position
\e[1;1H - move the cursor to row 1, column 1 (numbered from the upper left-hand corner
\e[u - restore the cursor to the previously saved position
\e42m - make the background green
\e0m - restore the default foreground/background colors
\[...\] - enclose the various non-printing characters so that bash can correctly compute the length of the prompt.
Wikipedia lists other escape codes. The two things missing from this answer are how to extend the bar all the way across the string and how to set the correct IP address.
Update: I believe this covers the changes that ruckc made:
PS1='\[\e[s\e[1;1H\e[42m\e[K\h \u ipaddress\e[0m\e[u\]\w \$ '
How about add a \n inside your PS1, so that you always use a new line with full width?
if you are looking for something less hacky (but maybe overkill), consider byobu
https://en.wikipedia.org/wiki/Byobu_(software)
Alternatively, if you are using xterms, you could set the xterm title instead:
export PS1="\[\033]0;\u $(host $(hostname))\007\]\u#\h:\w\$ "
This sets your xterm title, and sets your prompt to contain username#host:pwd.
My .bashrc contains something like this so PS1 is set correctly depending on whether we're in an xterm or not:
if [[ -n "$TERM" ]] ; then
if ( echo $TERM | $GREP -q xterm ) ; then
export PS1="\[\033]0;\u#\h:\w\007\]\u#\h:\w\$ "
else
export PS1="\u#\h:\w\$ "
fi
fi