Reputation: 21
import time
import sys
def animate_string(input_string, delay_second=0.01):
current_string = ""
for target_char in input_string:
if target_char == '\n':
# Print a newline and reset current_string
print()
current_string = ""
continue
current_char = 'a'
while current_char != target_char:
a_code = ord(current_char)
# previous string + current letter
sys.stdout.write('\r' + current_string + current_char)
sys.stdout.flush()
time.sleep(delay_second)
# Move to the next char (used ASCII)
current_char = chr(ord(current_char) + 1)
# Handle bound issue
if current_char == '{': # After 'z'
current_char = 'A'
elif current_char == '[': # After 'Z'
current_char = target_char
# Add the target character to the current string
current_string += target_char
# Print the updated current string
sys.stdout.write('\r' + current_string)
sys.stdout.flush()
# Print a newline at the end to finish the output cleanly
print()
animate_string("fjweiofjewoifjweofjewofjewffewjfiowjfwaifjewipfajeehgpwth235r2fjweiofjewoifjweofjewofjewffewjfiowjfwaifjewipfajeehgpwth235r2fjweiofjewoifjweofjewofjewffewjfiowjfwaifjewipfajeehgpwth235r2fjweiofjewoifjweofjewofjewffewjfiowjfwaifjewipfajeehgpwth235r2", 0.0002)
This is my code. I am working on printing string in a cool way. It works fine with small number of characters, but when the string becomes long enough to reach the end of terminal, it starts printing the same line over and over. why does this happen and how can I solve the issue?
Upvotes: 2
Views: 100
Reputation: 125
As pointed out in one of the comments, a carriage return ("\r"
) moves the cursor to the start of the current line. When text requires more terminal columns to display than the width of the terminal, the text is displayed across multiple lines. Thus, when writing a "long" string to the terminal, the cursor will end on a different line than the one on which the output started. Any subsequent "\r"
then moves the cursor to the start of this final line.
Assume that the terminal width is 50 columns, and you attempt to animate the 60-character string "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12345678"
.
When target_char
contains the 41st character (O
), this is the result of the first subsequent sys.stdout.write
call (with line numbers):
1 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNa
^ ^ cursor is here
cursor will be returned here by "\r"
When target_char
contains the 51st character (Y
), this is the result of the first subsequent sys.stdout.write
call (with line numbers):
1 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX
2 a
^^ cursor is here
cursor will be returned here by "\r"
The next call to sys.stdout.write
(when current_char
contains b
) will move the cursor to the start of the second line and overwrite only the a
, resulting in:
1 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX
2 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX
3 b
^^ cursor is here
cursor will be returned here by "\r"
From this point onward, every sys.stdout.write
call will add a new line to the output.
One solution is to overwrite only a single character, rather than the whole line, during the "cycling" animation. Using "\b"
to move the cursor back one column initially appears to work, but, for some reason, does not "wrap backwards" across lines. However, using an ANSI control sequence to set the cursor position does work:
import os
import sys
import time
def animate_string(input_string, delay_second=0.001):
stdout = sys.stdout
if not stdout.isatty():
# Standard output is not a terminal; just output the string
stdout.write(f"{input_string}\n")
return
# Get the size of the terminal to control line breaks correctly
cols, rows = os.get_terminal_size(stdout.fileno())
# Assume we are at the start of a new line
current_col = 1
for target_char in input_string:
if target_char == "\n":
# Write a newline and reset `current_col`
stdout.write(target_char)
current_col = 1
continue
# Escape sequence that sets the horizontal position of the cursor to `current_col`
# Setting this to "\b" does not work for long strings
undo_seq = f"\x1b[{current_col}G"
current_char = "a"
time.sleep(delay_second)
stdout.write(current_char)
stdout.flush()
while current_char != target_char:
# Move to the next char (used ASCII)
current_char = chr(ord(current_char) + 1)
# Handle bound issue
if current_char == "{": # After 'z'
current_char = "A"
elif current_char == "[": # After 'Z'
current_char = target_char
time.sleep(delay_second)
# "Undo" sequence followed by current letter
stdout.write(f"{undo_seq}{current_char}")
stdout.flush()
if current_col >= cols:
# Detect line wrapping performed by the terminal
current_col = 1
else:
current_col += 1
# Print a newline at the end to finish the output cleanly
stdout.write("\n")
We use os.get_terminal_size
to get the terminal width. We keep track of the current column in current_col
and use the CHA
ANSI control sequence to reset the cursor position during the "cycling" animation.
Additionally, we use stdout.isatty
to check if standard output is a terminal, and, if not, we simply write the input out.
Finally, I've moved the time.sleep
calls around a bit to make the timing more consistent.
This solution is not perfect and relies on the following assumptions:
For slightly more advanced control of the cursor, this question and its answers, as well as the Wikipedia entry on ANSI escape sequences, might be a good starting point. The termios
and tty
modules in the Python standard library might also be of interest to you, although they are not available everywhere.
Upvotes: 1