From d00a31fb4591521f16e30841e0d57f04842c3fef Mon Sep 17 00:00:00 2001 From: Leroy Hopson Date: Sun, 3 Mar 2024 22:25:14 +1300 Subject: [PATCH] feat(term): implement clear() method Clears all but the bottommost row of the terminal (including scrollback buffer) and moves the bottommost row to the top. --- addons/godot_xterm/native/src/terminal.cpp | 17 +++++++-- addons/godot_xterm/native/src/terminal.h | 2 ++ test/test_rendering.gd | 41 ++++++++++++++++++++-- test/test_terminal.gd | 34 ++++++++++++++---- 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/addons/godot_xterm/native/src/terminal.cpp b/addons/godot_xterm/native/src/terminal.cpp index 2fd6baa..4a2c5ec 100644 --- a/addons/godot_xterm/native/src/terminal.cpp +++ b/addons/godot_xterm/native/src/terminal.cpp @@ -75,8 +75,8 @@ void Terminal::_bind_methods() ClassDB::bind_method(D_METHOD("get_copy_on_selection"), &Terminal::get_copy_on_selection); ClassDB::add_property("Terminal", PropertyInfo(Variant::BOOL, "copy_on_selection"), "set_copy_on_selection", "get_copy_on_selection"); - // Methods. - + // Other methods. + ClassDB::bind_method(D_METHOD("clear"), &Terminal::clear); ClassDB::bind_method(D_METHOD("write", "data"), &Terminal::write); ClassDB::bind_method(D_METHOD("get_cursor_pos"), &Terminal::get_cursor_pos); ClassDB::bind_method(D_METHOD("get_cell_size"), &Terminal::get_cell_size); @@ -642,6 +642,19 @@ double Terminal::get_blink_off_time() const return blink_off_time; } +void Terminal::clear() { + // Resize the terminal to a single row, forcing content above in to the scrollback buffer. + tsm_screen_resize(screen, cols, 1); + + // Clear the scrollback buffer (hence clearing the content that was above). + tsm_screen_clear_sb(screen); + + // Resize the screen to its original size. + tsm_screen_resize(screen, cols, rows); + + refresh(); +} + String Terminal::_copy_screen(ScreenCopyFunction func) { char *out; PackedByteArray data; diff --git a/addons/godot_xterm/native/src/terminal.h b/addons/godot_xterm/native/src/terminal.h index a87ae0c..5ee7d34 100644 --- a/addons/godot_xterm/native/src/terminal.h +++ b/addons/godot_xterm/native/src/terminal.h @@ -72,6 +72,8 @@ namespace godot void set_blink_off_time(const double p_blink_off_time); double get_blink_off_time() const; + void clear(); + String copy_all(); String copy_selection(); void set_copy_on_selection(const bool p_enable); diff --git a/test/test_rendering.gd b/test/test_rendering.gd index 64fee5a..98d9a33 100644 --- a/test/test_rendering.gd +++ b/test/test_rendering.gd @@ -12,7 +12,7 @@ func get_described_class(): func pick_cell_color(cell := Vector2i(0, 0)) -> Color: var cell_size = subject.get_cell_size() var pixelv = Vector2(cell) * cell_size + (cell_size / 2) - return get_viewport().get_texture().get_image().get_pixelv(cell_size / 2) + return get_viewport().get_texture().get_image().get_pixelv(pixelv) func before_each(): @@ -27,9 +27,20 @@ func before_each(): class TestRendering: extends RenderingTest + # Fill the terminal with colored blocks. + func fill_color(color = Color.BLACK, rows: int = subject.get_rows()): + subject.write("\u001b[38;2;%d;%d;%dm" % [color.r8, color.g8, color.b8]) + subject.write("█".repeat(subject.get_cols() * rows)) + + func test_render(): + fill_color(Color.BLUE) + await wait_for_signal(subject.draw, 3) + await wait_frames(15) + var cell_color = pick_cell_color() + assert_eq(cell_color, Color.BLUE) + func test_update(): - subject.write("\u001b[38;2;255;0;0m") - subject.write("█".repeat(subject.get_cols() * subject.get_rows())) + fill_color(Color.RED) await get_tree().physics_frame subject.queue_redraw() await wait_for_signal(subject.draw, 3) @@ -37,6 +48,30 @@ class TestRendering: var cell_color = pick_cell_color(Vector2i(0, 0)) assert_eq(cell_color, Color.RED) + func test_clear_clears_all_but_the_first_row(): + await wait_frames(15) + var cell = Vector2i(0, 1) # Pick a cell not on the first row. + var original_color = pick_cell_color(cell) + call_deferred("fill_color", Color.CYAN) + await wait_for_signal(subject.draw, 3) + await wait_frames(15) + subject.clear() + await wait_for_signal(subject.draw, 3) + await wait_frames(15) + var cell_color = pick_cell_color(cell) + assert_eq(cell_color, original_color) + + func test_clear_keeps_the_last_row(): + fill_color(Color.GREEN) + fill_color(Color.ORANGE, 1) + await wait_for_signal(subject.draw, 3) + await wait_frames(15) + subject.clear() + await wait_for_signal(subject.draw, 3) + await wait_frames(15) + var cell_color = pick_cell_color(Vector2i(0, 0)) + assert_eq(cell_color, Color.ORANGE) + class TestKeyPressed: extends RenderingTest diff --git a/test/test_terminal.gd b/test/test_terminal.gd index ded9651..8775db7 100644 --- a/test/test_terminal.gd +++ b/test/test_terminal.gd @@ -13,6 +13,13 @@ func before_each(): subject.size = Vector2(400, 200) +# Helper function to fill the screen with the given character. +func fill_screen(char: String = "A") -> String: + var result = char.repeat(subject.get_cols() * subject.get_rows()) + subject.write(result) + return result + + class TestInterface: extends TerminalTest @@ -44,8 +51,7 @@ class TestInterface: # Methods. - # TODO: Implement clear() method. - func xtest_has_method_clear(): + func test_has_method_clear(): assert_has_method_with_return_type("clear", TYPE_NIL) func test_has_method_copy_all(): @@ -196,11 +202,6 @@ class TestWrite: class TestCopy: extends TerminalTest - func fill_screen(char: String = "A") -> String: - var result = char.repeat(subject.get_cols() * subject.get_rows()) - subject.write(result) - return result - func test_copy_all_copies_the_entire_screen(): var text = fill_screen() # The text will be wrapped over multiple lines and copy_all() preserves @@ -220,3 +221,22 @@ class TestCopy: var text = "アイウエオカキクケコサシスセソ" subject.write(text) assert_string_contains(subject.copy_all(), text) + + +class TestClear: + extends TerminalTest + + func test_clear_an_empty_screen_changes_nothing(): + var empty_screen = subject.copy_all() + subject.clear() + var screen_after = subject.copy_all() + assert_eq(screen_after, empty_screen) + + func test_clear_when_screen_is_full_clears_all_but_the_bottommost_row(): + fill_screen() + var final_line = "THIS SHOULDN'T BE CLEARED" + subject.write(final_line) + subject.clear() + var screen_after = subject.copy_all() + var expected = final_line + "\n".repeat(subject.get_rows()) + assert_eq(screen_after, expected)