Overwriting previous terminal output with built-in functions

This is heavily related to How clear the printed content in terminal and print to the same line?, which provides some good answers.

The linked thread has examples that I used to create the showcase function below:

julia> function test()
               print("ayy")
               print("\e[2K") # clear whole line
               print("\e[1G") # move cursor to column 1
               print("new line")
       end
test (generic function with 1 method)

julia> test()
new line

So it seems like these escape sequences can do what is needed. However, I just stumbled upon a bunch of functions inside REPL.LineEdit, with promising names. I was hoping they would be easier to reason about than obscure codes. Below is the output from tab-completing the functions in the module that start with edit_:

julia> import REPL

julia> REPL.LineEdit.edit_

edit_abort                    edit_backspace
edit_clear                    edit_copy_region
edit_delete                   edit_delete_next_word
edit_delete_prev_word         edit_exchange_point_and_mark
edit_indent                   edit_indent_left
edit_indent_right             edit_input
edit_insert                   edit_insert_last_word
edit_insert_newline           edit_insert_tab
edit_kill_line                edit_kill_line_backwards
edit_kill_line_forwards       edit_kill_region
edit_lower_case               edit_move_down
edit_move_left                edit_move_right
edit_move_up                  edit_move_word_left
edit_move_word_right          edit_redo!
edit_replace_word_right       edit_shift_move
edit_splice!                  edit_tab
edit_title_case               edit_transpose_chars
edit_transpose_lines_down!    edit_transpose_lines_up!
edit_transpose_words          edit_undo!
edit_upper_case               edit_werase
edit_yank                     edit_yank_pop

Are these functions something that users could/should use? Are there other, better ways to overwrite previous terminal outputs? Or is the status just “get good and learn ANSI escape codes”?

It looks like REPL.Terminals has what I am looking for in the functions starting with cmove:

julia> REPL.Terminals.cmove

cmove            cmove_col        cmove_down
cmove_left       cmove_line_down  cmove_line_up
cmove_right      cmove_up

It just requires a terminal as the first argument, which you can get a handle to via the following line (From this discourse discussion on the topic):

import REPL
terminal = REPL.Terminals.TTYTerminal(string(), stdin, stdout, stderr)

By inspecting the source, it turns out the these functions are exactly just pretty packaging of the escape sequences. Here are lines 113 to 121 from the file defining REPL.Terminals.clear_line

const CSI = "\x1b["

cmove_up(t::UnixTerminal, n) = write(t.out_stream, "$(CSI)$(n)A")
cmove_down(t::UnixTerminal, n) = write(t.out_stream, "$(CSI)$(n)B")
cmove_right(t::UnixTerminal, n) = write(t.out_stream, "$(CSI)$(n)C")
cmove_left(t::UnixTerminal, n) = write(t.out_stream, "$(CSI)$(n)D")
cmove_line_up(t::UnixTerminal, n) = (cmove_up(t, n); cmove_col(t, 1))
cmove_line_down(t::UnixTerminal, n) = (cmove_down(t, n); cmove_col(t, 1))
cmove_col(t::UnixTerminal, n) = (write(t.out_stream, '\r'); n > 1 && cmove_right(t, n-1))

With this, I was able to put together this example, completely without learning the codes. Sweet!

import REPL
terminal = REPL.Terminals.TTYTerminal(string(), stdin, stdout, stderr)
println()  # Dummy line
for i in (1, "two", :III, "||||")
	REPL.Terminals.cmove_left(terminal, length(string(i)))
	REPL.Terminals.cmove_line_up(terminal)
	println(i)
	sleep(0.5)
end
println("Watch the final number dissapear above me")
sleep(0.5)
REPL.Terminals.cmove_line_up(terminal, 2)
REPL.Terminals.clear_line(terminal)
REPL.Terminals.cmove_line_down(terminal, 3)  # If not, next input is above previous output, which looks funny

3 Likes

This is very interesting! Do you think this could overwrite input as well?

I’m working on VimBindings.jl and am looking for ways to implement Replace mode, I’m interested in using some terminal hacking for that.

It seemed like one could move the cursor arbitrarily, so anything in the terminal should be fair game. That would naturally include previous input.

If you want changed to the current input, then I suspect the functions inside LineEdit from the original post are what you want. They do however not take a terminal as argument, and I was unable to understand how to use them based on their methods.

1 Like