How does a terminal "ASCII animation" work? - shell

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.

Related

bash prinf \r multiple line show only one line

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.

zsh truncates previous stdout when prompt width is same as COLUMNS

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.

Bash - Removing white space from indented multiline strings

This may be a more general question so sorry in advance. I am creating a script and thought it would be good to use multi-line strings instead of using multiple printf or echo statements. Say I have the following:
while :
do
printf "line 1
line 2
line 3"
done
The second and third lines would be printed with a space in front because of the indentation in the file.
l1
line 2
line 3
Is there a way to prevent that aside from removing the indentation on the code? Also, is it considered a better practice to just multiple printf/echo statements if you need to output information that spans multiple lines?
Indent with tabs (here whitespace) and use a heredoc (with <<-)
cat <<- EOF
line 1
line 2
line 3
EOF
Multi-line strings will always look a bit bad, or have some other downsides, I'm afraid. The most legible way to embed them in bash code is probably the here-doc, which shows the string (almost) exactly like it will look when output. As an extra knack, you can use extra punctuation to make the here-doc delimiter to stand out from the string itself too, like so:
if true
then
some commands
cat <<"____EndOfTextBlock____"
This text here
spans multiple
lines.
____EndOfTextBlock____
some other commands
even more commands
fi

.tmux.conf color codes causing strange output

Follow-up to: Colored text printing spaces in shell script
I have a script I wrote (with some help from #Barmar) which displays my current CPU and memory load visually. The output looks like so:
I then put the following into my .tmux.conf file:
set -g status-right "#(~/load.sh)"
I reload my tmux config and get the following output in the bottom-right:
There are two issues:
The CPU section should contain 11 characters: a "clear color code" character (tput sgr0) and 10 spaces. Instead it contains (B[m
The MEM section... should exist. The entire [| ] has turned into a y> -- I don't even know how the square bracket is missing, that should get printed before any color codes or weird control characters
Can tmux status bars simply not contain color?
tmux status bars don't use ANSI escape codes, they use the same color code format as other things in tmux. You want something more like (assuming 256-color mode):
#[fg=colour28 bg=colour250]Hello World!

In an xterm, can I turn off bold or underline without resetting the current color?

I'm processing input (from sources like, but limited to, ls -la --color) and underlining certain sections of the text. I don't process these inputs character-by-character, but with lots of regular expressions, which makes it rather difficult to track whether the substring I'm affecting is already colored or in bold. If I have a red block of text, and I want to underline some part of it, I might do something like:
s/(123)/\033[4m\1\033[0m/g
(My expressions are much more complex. In reality, matches extracted, processed on their own, and further broken down and analyzed. This isn't something that can be done by changing the expression I've given here.)
The code above would replace all ocurrences of 123 with [UNDERLINE_START]123[FORMAT_RESET]. Unfortunately, the reset also turns off the coloring of text. It would save me a great deal of headache if I could just turn off the underlining when that is all I want to disable, but I'm pretty sure there's no way to do that. Can anyone tell me that I'm wrong?
EDIT: I could simplify the question by asking: if I want to turn off underlining, can I do that without affecting the current text color, since that color could have been set long before my script even started executing, and I don't have a way to detect what that color is?
Nobody seems to document it, but adding 20 to the decorator codes will turn them off:
echo -e "\\033[34;4m" underlined "\\033[24m" not underlined
echo -e "\\033[34;1m" bold "\\033[2m" not bold
echo -e "\\033[34;2m" dark "\\033[22m" not dark
echo -e "\\033[34;7m" inverse "\\033[27m" not inverse
Instead of those hard-coded escape sequences, use:
tput smul # set underline
tput rmul # remove underline
tput smso # set bold on
tput rmso # remove bold
tput setaf 1 #red
tput setaf 2 #green
...
tput cup 0 0 # move to pos 0,0
Refer to "man terminfo" and "man tput" for complete descriptions of these commands.
Examples :
function f_help
{
c_green=$(tput setaf 2 2>/dev/null)
c_reset=$(tput sgr0 2>/dev/null)
c_bold=$(tput smso 2>/dev/null)
echo "${c_bold}DESCRIPTION${c_reset} : ...."
}
echo "${c_green}GREEN ${c_underline} GREEN AND UNDERLINED${c_nounderline} GREEN AGAIN${c_reset} PLAIN BLACK TEXT"
The ${c_underline} escape sequence doesn't affect the color of the text; it only turns underlining on. ${c_nounderline} only turns underlining off.

Resources