diff --git a/addons/godot_xterm/native/SConstruct b/addons/godot_xterm/native/SConstruct index 6ced2fd..28337cf 100644 --- a/addons/godot_xterm/native/SConstruct +++ b/addons/godot_xterm/native/SConstruct @@ -1,66 +1,39 @@ #!/usr/bin/env python -# SPDX-FileCopyrightText: 2020-2023 Leroy Hopson +# SPDX-FileCopyrightText: 2020-2024 Leroy Hopson # SPDX-License-Identifier: MIT import os -import sys -env = SConscript("./thirdparty/godot-cpp/SConstruct") +env = SConscript("thirdparty/godot-cpp/SConstruct") env['ENV'] = os.environ -opts = Variables([], ARGUMENTS) -opts.Add(BoolVariable( - 'disable_pty', - 'Disables the PTY and its dependencies (LibuvUtils and Pipe). Has no effect on platforms where PTY is not supported', - False -)) -opts.Update(env) -Help(opts.GenerateHelpText(env)) +VariantDir('build', 'src', duplicate=0) +env['OBJPREFIX'] = os.path.join('build', '') env.Append(CPPPATH=[ - 'src/', - 'thirdparty/libtsm/src/tsm', - 'thirdparty/libtsm/external', - 'thirdparty/libtsm/src/shared', - 'thirdparty/libuv/include', + "thirdparty/libtsm/src/tsm", + "thirdparty/libtsm/external", + "thirdparty/libtsm/src/shared", ]) -sources = Glob('thirdparty/libtsm/src/tsm/*.c') +sources = Glob("src/*.cpp") + Glob("thirdparty/libtsm/src/tsm/*.c") sources.append([ 'thirdparty/libtsm/external/wcwidth/wcwidth.c', 'thirdparty/libtsm/src/shared/shl-htable.c', - 'src/register_types.cpp', - 'src/constants.cpp', - 'src/terminal.cpp', ]) -if env['disable_pty'] or env['platform'] == 'javascript': - env.Append(CPPDEFINES=['_PTY_DISABLED']) +if env["platform"] == "macos": + library = env.SharedLibrary( + "bin/libgodot-xterm.{}.{}.framework/libgodot-xterm.{}.{}".format( + env["platform"], env["target"], env["platform"], env["target"] + ), + source=sources, + ) else: - sources.append('src/pipe.cpp') - sources.append('src/libuv_utils.cpp') - if env['platform'] != 'windows': - sources.append('src/node_pty/unix/pty.cc') - env.Append(LIBS=['util', env.File('thirdparty/libuv/build/libuv_a.a')]) - else: - sources.append('src/node_pty/win/conpty.cc') - env.Append(LIBS=[ - env.File('thirdparty/libuv/build/{}/uv_a.lib'.format(env["target"].capitalize())), - 'Advapi32.lib', - 'Iphlpapi.lib', - 'user32.lib', - 'userenv.lib', - 'Ws2_32.lib', - ]) - -env.Append(LINKFLAGS=['-static-libstdc++']) - -library = env.SharedLibrary( - target='bin/libgodot-xterm{}{}'.format( - env['suffix'], - env['SHLIBSUFFIX'], - ), source=sources -) + library = env.SharedLibrary( + "bin/libgodot-xterm{}{}".format(env["suffix"], env["SHLIBSUFFIX"]), + source=sources, + ) Default(library) diff --git a/addons/godot_xterm/native/bin/.gitignore b/addons/godot_xterm/native/bin/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/addons/godot_xterm/native/bin/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/addons/godot_xterm/native/bin/godot-xterm.gdextension b/addons/godot_xterm/native/bin/godot-xterm.gdextension new file mode 100644 index 0000000..4e704b1 --- /dev/null +++ b/addons/godot_xterm/native/bin/godot-xterm.gdextension @@ -0,0 +1,19 @@ +[configuration] + +entry_symbol = "godot_xterm_library_init" +compatibility_minimum = "4.2.1" + +[libraries] + +macos.debug = "res://addons/godot_xterm/native/bin/libgodot-xterm.macos.template_debug.framework" +macos.release = "res://addons/godot_xterm/native/bin/libgodot-xterm.macos.template_release.framework" +windows.debug.x86_32 = "res://addons/godot_xterm/native/bin/libgodot-xterm.windows.template_debug.x86_32.dll" +windows.release.x86_32 = "res://addons/godot_xterm/native/bin/libgodot-xterm.windows.template_release.x86_32.dll" +windows.debug.x86_64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.windows.template_debug.x86_64.dll" +windows.release.x86_64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.windows.template_release.x86_64.dll" +linux.debug.x86_64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_debug.x86_64.so" +linux.release.x86_64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_release.x86_64.so" +linux.debug.arm64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_debug.arm64.so" +linux.release.arm64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_release.arm64.so" +linux.debug.rv64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_debug.rv64.so" +linux.release.rv64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_release.rv64.so" \ No newline at end of file diff --git a/addons/godot_xterm/native/build.sh b/addons/godot_xterm/native/build.sh index 738783a..e7dbc86 100755 --- a/addons/godot_xterm/native/build.sh +++ b/addons/godot_xterm/native/build.sh @@ -53,7 +53,7 @@ updateSubmodules() { } updateSubmodules LIBUV_DIR ${NATIVE_DIR}/thirdparty/libuv -updateSubmodules LIBTSM_DIR ${NATIVE_DIR}/thirdparty/libtsm +updateSubmodules LIBTSM_DIR ${NATIVE_DIR}/thirdparty/libtsm updateSubmodules GODOT_CPP_DIR ${NATIVE_DIR}/thirdparty/godot-cpp # Build libuv as a static library. diff --git a/addons/godot_xterm/native/docker-compose.yml b/addons/godot_xterm/native/docker-compose.yml index 4298de6..245e9b4 100644 --- a/addons/godot_xterm/native/docker-compose.yml +++ b/addons/godot_xterm/native/docker-compose.yml @@ -1,59 +1,59 @@ --- services: javascript: - build: - context: . - dockerfile: javascript.Dockerfile - user: ${UID_GID} - volumes: - - .:/src - command: - - /bin/bash - - -c - - | - cd /src/thirdparty/godot-cpp - scons platform=javascript target=$${TARGET:-debug} -j$$(nproc) - cd /src - scons platform=javascript target=$${TARGET:-debug} -j$$(nproc) + build: + context: . + dockerfile: javascript.Dockerfile + user: ${UID_GID} + volumes: + - .:/src + command: + - /bin/bash + - -c + - | + cd /src/thirdparty/godot-cpp + scons platform=javascript target=$${TARGET:-debug} -j$$(nproc) + cd /src + scons platform=javascript target=$${TARGET:-debug} -j$$(nproc) libuv-linux: - user: ${UID_GID} - build: - context: . - dockerfile: linux.Dockerfile - volumes: - - ./thirdparty/libuv:/libuv - working_dir: /libuv - command: - - /bin/bash - - -c - - | - target=$${TARGET:-release} - arch=$${ARCH:-x86_64} - mkdir build 2>/dev/null ; - args="-DCMAKE_BUILD_TYPE=$$target \ - -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE" - if [[ $$arch == "x86_32" ]]; then - args="$$args -DCMAKE_SYSTEM_PROCESSOR=i686 -DCMAKE_C_FLAGS=-m32"; - else - args="$$args -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_C_FLAGS="; - fi - pushd build - cmake .. $$args - popd - cmake --build build + user: ${UID_GID} + build: + context: . + dockerfile: linux.Dockerfile + volumes: + - ./thirdparty/libuv:/libuv + working_dir: /libuv + command: + - /bin/bash + - -c + - | + target=$${TARGET:-release} + arch=$${ARCH:-x86_64} + mkdir build 2>/dev/null ; + args="-DCMAKE_BUILD_TYPE=$$target \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE" + if [[ $$arch == "x86_32" ]]; then + args="$$args -DCMAKE_SYSTEM_PROCESSOR=i686 -DCMAKE_C_FLAGS=-m32"; + else + args="$$args -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_C_FLAGS="; + fi + pushd build + cmake .. $$args + popd + cmake --build build libgodot-xterm-linux: - user: ${UID_GID} - build: - context: . - dockerfile: linux.Dockerfile - volumes: - - .:/godot-xterm - working_dir: /godot-xterm - environment: - - SCONS_CACHE=/scons-cache - command: - - /bin/bash - - -c - - | - scons target=template_$${TARGET:-debug} arch=$${ARCH:-x86_64} + user: ${UID_GID} + build: + context: . + dockerfile: linux.Dockerfile + volumes: + - .:/godot-xterm + working_dir: /godot-xterm + environment: + - SCONS_CACHE=/scons-cache + command: + - /bin/bash + - -c + - | + scons target=template_$${TARGET:-debug} arch=$${ARCH:-x86_64} diff --git a/addons/godot_xterm/native/src/register_types.cpp b/addons/godot_xterm/native/src/register_types.cpp index b2de5af..94492ad 100644 --- a/addons/godot_xterm/native/src/register_types.cpp +++ b/addons/godot_xterm/native/src/register_types.cpp @@ -2,17 +2,6 @@ #include "terminal.h" -#if !defined(_PTY_DISABLED) -#include "libuv_utils.h" -#include "pipe.h" -#if defined(__linux__) || defined(__APPLE__) -#include "node_pty/unix/pty.h" -#endif -#if defined(__WIN32) -// #include "node_pty/win/conpty.h" -#endif -#endif - #include #include #include @@ -25,16 +14,6 @@ void initialize_godot_xterm_module(ModuleInitializationLevel p_level) { } ClassDB::register_class(); -#if !defined(_PTY_DISABLED) - ClassDB::register_class(); - ClassDB::register_class(); -#if defined(__linux__) || defined(__APPLE__) - ClassDB::register_class(); -#endif -#if defined(__WIN32) - // ClassDB::register_class(); -#endif -#endif } void uninitialize_godot_xterm_module(ModuleInitializationLevel p_level) { @@ -44,7 +23,7 @@ void uninitialize_godot_xterm_module(ModuleInitializationLevel p_level) { } extern "C" { -// Initialization +// Initialization. GDExtensionBool GDE_EXPORT godot_xterm_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, diff --git a/addons/godot_xterm/native/src/terminal.cpp b/addons/godot_xterm/native/src/terminal.cpp index 0957089..b963415 100644 --- a/addons/godot_xterm/native/src/terminal.cpp +++ b/addons/godot_xterm/native/src/terminal.cpp @@ -1,654 +1,450 @@ -// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson +// SPDX-FileCopyrightText: 2021-2024 Leroy Hopson // SPDX-License-Identifier: MIT #include "terminal.h" -#include -#include -#include -#include + +#include +#include +#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include -#define UNICODE_MAX 0x10FFFF +#define SHADERS_DIR "res://addons/godot_xterm/shaders/" +#define FOREGROUND_SHADER_PATH SHADERS_DIR "foreground.gdshader" +#define BACKGROUND_SHADER_PATH SHADERS_DIR "background.gdshader" using namespace godot; -Terminal::Terminal() { - // Ensure we write to terminal before the frame is drawn. Otherwise, the - // terminal state may be updated but not drawn until it is updated again, - // which may not happen for some time. - RenderingServer::get_singleton()->connect("frame_pre_draw", - Callable(this, "_flush")); +void Terminal::_bind_methods() +{ + ClassDB::bind_method(D_METHOD("get_cols"), &Terminal::get_cols); + ClassDB::bind_method(D_METHOD("set_cols", "cols"), &Terminal::set_cols); + ClassDB::add_property("Terminal", PropertyInfo(Variant::INT, "cols"), "set_cols", "get_cols"); - // Override default focus mode. - set_focus_mode(FOCUS_ALL); + ClassDB::bind_method(D_METHOD("get_rows"), &Terminal::get_rows); + ClassDB::bind_method(D_METHOD("set_rows", "rows"), &Terminal::set_rows); + ClassDB::add_property("Terminal", PropertyInfo(Variant::INT, "rows"), "set_rows", "get_rows"); - // Name our nodes for easier debugging. - back_buffer->set_name("BackBuffer"); - sub_viewport->set_name("SubViewport"); - front_buffer->set_name("FrontBuffer"); + ClassDB::bind_method(D_METHOD("get_max_scrollback"), &Terminal::get_max_scrollback); + ClassDB::bind_method(D_METHOD("set_max_scrollback", "max_scrollback"), &Terminal::set_max_scrollback); + ClassDB::add_property("Terminal", PropertyInfo(Variant::INT, "max_scrollback"), "set_max_scrollback", "get_max_scrollback"); - // Ensure buffers always have correct size. - back_buffer->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT); - front_buffer->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT); - - // Setup back buffer. - back_buffer->connect("draw", Callable(this, "_on_back_buffer_draw")); - - // Setup sub viewport. - sub_viewport->set_handle_input_locally(false); - sub_viewport->set_transparent_background(true); - sub_viewport->set_snap_controls_to_pixels(false); - sub_viewport->set_update_mode(SubViewport::UPDATE_WHEN_PARENT_VISIBLE); - sub_viewport->set_clear_mode(SubViewport::CLEAR_MODE_NEVER); - sub_viewport->add_child(back_buffer); - add_child(sub_viewport); - - // Setup bell timer. - bell_timer->set_name("BellTimer"); - bell_timer->set_one_shot(true); - add_child(bell_timer); - - // Setup blink timer. - blink_timer->set_name("BlinkTimer"); - blink_timer->set_one_shot(true); - blink_timer->connect("timeout", Callable(this, "_toggle_blink")); - add_child(blink_timer); - - // Setup selection timer. - selection_timer->set_name("SelectionTimer"); - selection_timer->set_wait_time(0.05); - selection_timer->connect("timeout", Callable(this, "_on_selection_held")); - add_child(selection_timer); - - // Setup front buffer. - front_buffer->set_texture(sub_viewport->get_texture()); - add_child(front_buffer); - - framebuffer_age = 0; - update_mode = UpdateMode::AUTO; - - if (tsm_screen_new(&screen, NULL, NULL)) { - ERR_PRINT("Error creating new tsm screen."); - } - tsm_screen_set_max_sb(screen, 1000); - - if (tsm_vte_new(&vte, screen, &Terminal::_write_cb, this, NULL, NULL)) { - ERR_PRINT("Error creating new tsm vte."); - } - - tsm_vte_set_bell_cb(vte, &Terminal::_bell_cb, this); - - _update_theme_item_cache(); + ClassDB::bind_method(D_METHOD("write", "data"), &Terminal::write); } -Terminal::~Terminal() { - back_buffer->queue_free(); - sub_viewport->queue_free(); - front_buffer->queue_free(); +Terminal::Terminal() +{ + max_scrollback = 1000; + + + if (tsm_screen_new(&screen, NULL, NULL)) + { + ERR_PRINT("Failed to create tsm screen."); + } + tsm_screen_set_max_sb(screen, max_scrollback); + + if (tsm_vte_new(&vte, screen, &Terminal::_write_cb, this, NULL, NULL)) + { + ERR_PRINT("Failed to create tsm vte."); + } + + initialize_rendering(); + update_theme(); + update_sizes(); } -void Terminal::set_copy_on_selection(bool value) { copy_on_selection = value; } - -bool Terminal::get_copy_on_selection() { return copy_on_selection; } - -void Terminal::set_update_mode(Terminal::UpdateMode value) { - update_mode = value; -}; - -Terminal::UpdateMode Terminal::get_update_mode() { return update_mode; } - -void Terminal::set_bell_cooldown(double value) { bell_cooldown = value; } - -double Terminal::get_bell_cooldown() { return bell_cooldown; } - -void Terminal::set_bell_muted(bool value) { bell_muted = value; } - -bool Terminal::get_bell_muted() { return bell_muted; } - -void Terminal::set_blink_enabled(bool value) { - blink_enabled = value; - - if (!blink_enabled) - blink_timer->stop(); - - _refresh(); +Terminal::~Terminal() +{ + cleanup_rendering(); } -bool Terminal::get_blink_enabled() { return blink_enabled; } - -void Terminal::set_blink_time_off(double value) { - blink_time_off = value; - - if (!blink_on && !blink_timer->is_stopped()) { - double time_left = blink_timer->get_time_left(); - blink_timer->start(std::min(blink_time_off, time_left)); - } +void Terminal::set_cols(const int p_cols) +{ + cols = p_cols; } -double Terminal::get_blink_time_off() { return blink_time_off; } - -void Terminal::set_blink_time_on(double value) { - blink_time_on = value; - - if (blink_on && !blink_timer->is_stopped()) { - double time_left = blink_timer->get_time_left(); - blink_timer->start(std::min(blink_time_on, time_left)); - } +int Terminal::get_cols() const +{ + return cols; } -double Terminal::get_blink_time_on() { return blink_time_on; } - -void Terminal::clear() { - Vector2 initial_size = get_size(); - set_size(Vector2(initial_size.x, cell_size.y)); - tsm_screen_clear_sb(screen); - set_size(initial_size); - back_buffer->queue_redraw(); +void Terminal::set_rows(const int p_rows) +{ + rows = p_rows; } -String Terminal::copy_all() { - char *out = nullptr; - int len = tsm_screen_copy_all(screen, &out); - String result = String(out); - std::free(out); - return result; +int Terminal::get_rows() const +{ + return rows; } -String Terminal::copy_selection() { - char *out = nullptr; - int len = tsm_screen_selection_copy(screen, &out); - String result = String(out); - std::free(out); - return result; +void Terminal::set_max_scrollback(const int p_max_scrollback) +{ + max_scrollback = std::max(0, p_max_scrollback); + tsm_screen_set_max_sb(screen, max_scrollback); } -int Terminal::get_cols() { return cols; } -int Terminal::get_rows() { return rows; } - -void Terminal::write(Variant data) { - switch (data.get_type()) { - case Variant::PACKED_BYTE_ARRAY: - break; - case Variant::STRING: - data = ((String)data).to_utf8_buffer(); - break; - default: - ERR_PRINT("Expected data to be a String or PackedByteArray."); - } - - write_buffer.push_back(data); - - queue_redraw(); +int Terminal::get_max_scrollback() const +{ + return max_scrollback; } -void Terminal::_gui_input(Ref event) { - _handle_key_input(event); - _handle_selection(event); - _handle_mouse_wheel(event); +void Terminal::write(Variant data) +{ + PackedByteArray bytes; + + switch (data.get_type()) + { + case Variant::STRING: + bytes = ((String)data).to_utf8_buffer(); + break; + case Variant::PACKED_BYTE_ARRAY: + bytes = data; + break; + default: + ERR_FAIL_MSG("Data must be a String or PackedByteArray."); + return; + } + + if (bytes.is_empty()) + return; + + tsm_vte_input(vte, (char *)bytes.ptr(), bytes.size()); + + queue_redraw(); } -void Terminal::_notification(int what) { - switch (what) { - case NOTIFICATION_RESIZED: - _recalculate_size(); - sub_viewport->set_size(get_size()); - _refresh(); - break; - case NOTIFICATION_THEME_CHANGED: - _update_theme_item_cache(); - _refresh(); - break; - } +void Terminal::_notification(int what) +{ + switch (what) + { + case NOTIFICATION_READY: + { + update_theme(); + update_sizes(true); + break; + } + case NOTIFICATION_THEME_CHANGED: + { + update_theme(); + } + case NOTIFICATION_RESIZED: + { + update_sizes(); + break; + } + case NOTIFICATION_DRAW: + { + draw_screen(); + break; + } + } } -void Terminal::_flush() { - if (write_buffer.is_empty()) - return; - - for (int i = 0; i < write_buffer.size(); i++) { - PackedByteArray data = static_cast(write_buffer[i]); - tsm_vte_input(vte, (char *)data.ptr(), data.size()); - } - - write_buffer.clear(); - - back_buffer->queue_redraw(); +void Terminal::_write_cb(tsm_vte *vte, const char *u8, size_t len, void *data) +{ + Terminal *term = static_cast(data); } -void Terminal::_on_back_buffer_draw() { - if (update_mode == UpdateMode::DISABLED) { - return; - } +int Terminal::_draw_cb(struct tsm_screen *con, + uint64_t id, + const uint32_t *ch, + size_t len, + unsigned int width, + unsigned int posx, + unsigned int posy, + const struct tsm_screen_attr *attr, + tsm_age_t age, + void *data) +{ + Terminal *term = static_cast(data); - if ((update_mode > UpdateMode::AUTO) || framebuffer_age == 0) { - Color background_color = palette[TSM_COLOR_BACKGROUND]; - back_buffer->draw_rect(back_buffer->get_rect(), background_color); - } + if (age != 0 && age <= term->framebuffer_age) return OK; - int prev_framebuffer_age = framebuffer_age; - framebuffer_age = tsm_screen_draw(screen, &Terminal::_text_draw_cb, this); + if (width < 1) + { // No foreground or background to draw. + return OK; + } - if (update_mode == UpdateMode::ALL_NEXT_FRAME && prev_framebuffer_age != 0) - update_mode = UpdateMode::AUTO; + // Update attributes. + int attr_flags = 0; + if (attr->inverse) + attr_flags |= AttrFlags::INVERSE; + if (attr->blink) + attr_flags |= AttrFlags::BLINK; + term->attr_image->set_pixel(posx, posy, Color(attr_flags / 255.0f, 0, 0, 0)); + + // Colors. + Color fgcol = std::min(attr->fccode, (int8_t)TSM_COLOR_FOREGROUND) >= 0 + ? term->palette[attr->fccode] + : Color(attr->fr / 255.0f, attr->fg / 255.0f, attr->fb / 255.0f); + + Color bgcol = std::min(attr->bccode, (int8_t)TSM_COLOR_BACKGROUND) >= 0 + ? term->palette[attr->bccode] + : Color(attr->br / 255.0f, attr->bg / 255.0f, attr->bb / 255.0f); + + // Draw background. + term->back_image->set_pixel(posx, posy, bgcol); + + if (len < 1) + { // No foreground to draw. + return OK; + } + + Vector2 cell_position = Vector2(posx * term->cell_size.x, posy * term->cell_size.y); + Rect2 cell_rect = Rect2(cell_position, term->cell_size); + + // Erase any previous character in the cell. + term->rs->canvas_item_add_rect(term->char_canvas_item, cell_rect, Color(1, 1, 1, 0)); + + term->font->draw_char( + term->char_canvas_item, + Vector2i(cell_position.x, cell_position.y + term->font->get_height( + term->font_size / + 1.25)), + String((char *)ch).unicode_at(0), + term->font_size, + fgcol + ); + + return OK; } -void Terminal::_on_selection_held() { - if (!(Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) || - selection_mode == SelectionMode::NONE) { - if (copy_on_selection) - DisplayServer::get_singleton()->clipboard_set_primary(copy_selection()); - selection_timer->stop(); - return; - } +bool Terminal::_set(const StringName &property, const Variant &value) +{ + if (property.begins_with("theme_override_colors/")) + { + String color_name = property.split("/")[1]; + if (_is_valid_color_name(color_name)) + { + if (value.get_type() != Variant::Type::NIL) + { + add_theme_color_override(color_name, value); + } + else + { + remove_theme_color_override(color_name); + } + return true; + } + } - Vector2 target = get_local_mouse_position() / cell_size; - tsm_screen_selection_target(screen, target.x, target.y); - back_buffer->queue_redraw(); - selection_timer->start(); + if (property.begins_with("theme_override_fonts/")) + { + String font_type = property.split("/")[1]; + if (_is_valid_font_type(font_type)) + { + if (value.get_type() != Variant::Type::NIL) + { + add_theme_font_override(font_type, value); + } + else + { + remove_theme_font_override(font_type); + } + return true; + } + } + + if (property == String("theme_override_font_sizes/font_size")) + { + if (value.get_type() != Variant::Type::NIL) + { + add_theme_font_size_override("font_size", value == Variant(0) ? Variant(1) : value); + } + else + { + remove_theme_font_size_override("font_size"); + } + return true; + } + + return false; } -void Terminal::_toggle_blink() { - if (blink_enabled) { - blink_on = !blink_on; - _refresh(); - } +void Terminal::_get_property_list(List *p_list) const +{ + p_list->push_back(PropertyInfo(Variant::NIL, "Theme Overrides", PROPERTY_HINT_NONE, "theme_override_", PROPERTY_USAGE_GROUP)); + p_list->push_back(PropertyInfo(Variant::NIL, "Colors", PROPERTY_HINT_NONE, "theme_override_colors/", PROPERTY_USAGE_SUBGROUP)); + for (int i = 0; i < TSM_COLOR_NUM; ++i) + { + p_list->push_back(PropertyInfo(Variant::COLOR, "theme_override_colors/" + String(COLOR_NAMES[i]), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_CHECKABLE)); + } + p_list->push_back(PropertyInfo(Variant::NIL, "Fonts", PROPERTY_HINT_NONE, "theme_override_fonts/", PROPERTY_USAGE_SUBGROUP)); + for (const String font_type : FONT_TYPES) + { + p_list->push_back(PropertyInfo(Variant::OBJECT, "theme_override_fonts/" + font_type, PROPERTY_HINT_RESOURCE_TYPE, "Font", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_CHECKABLE)); + } + p_list->push_back(PropertyInfo(Variant::NIL, "Font Sizes", PROPERTY_HINT_NONE, "theme_override_font_sizes/", PROPERTY_USAGE_SUBGROUP)); + p_list->push_back(PropertyInfo(Variant::INT, "theme_override_font_sizes/font_size", PROPERTY_HINT_RANGE, "1,256,1,or_greater,suffix:px", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_CHECKABLE)); } -void Terminal::_bind_methods() { - // Properties. - ClassDB::bind_method(D_METHOD("set_copy_on_selection", "value"), - &Terminal::set_copy_on_selection); - ClassDB::bind_method(D_METHOD("get_copy_on_selection"), - &Terminal::get_copy_on_selection); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "copy_on_selection"), - "set_copy_on_selection", "get_copy_on_selection"); - ClassDB::bind_method(D_METHOD("set_update_mode", "value"), - &Terminal::set_update_mode); - ClassDB::bind_method(D_METHOD("get_update_mode"), &Terminal::get_update_mode); - ADD_PROPERTY(PropertyInfo(Variant::INT, "update_mode", PROPERTY_HINT_ENUM, - "Disabled,Auto,All,All Next Frame"), - "set_update_mode", "get_update_mode"); - - ADD_GROUP("Bell", "bell_"); - ClassDB::bind_method(D_METHOD("set_bell_cooldown", "value"), - &Terminal::set_bell_cooldown); - ClassDB::bind_method(D_METHOD("get_bell_cooldown"), - &Terminal::get_bell_cooldown); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bell_cooldown"), - "set_bell_cooldown", "get_bell_cooldown"); - ClassDB::bind_method(D_METHOD("set_bell_muted", "value"), - &Terminal::set_bell_muted); - ClassDB::bind_method(D_METHOD("get_bell_muted"), &Terminal::get_bell_muted); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "bell_muted"), "set_bell_muted", - "get_bell_muted"); - - ADD_GROUP("Blink", "blink_"); - ClassDB::bind_method(D_METHOD("set_blink_enabled", "value"), - &Terminal::set_blink_enabled); - ClassDB::bind_method(D_METHOD("get_blink_enabled"), - &Terminal::get_blink_enabled); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "blink_enabled"), - "set_blink_enabled", "get_blink_enabled"); - ClassDB::bind_method(D_METHOD("get_blink_time_off"), - &Terminal::get_blink_time_off); - ClassDB::bind_method(D_METHOD("set_blink_time_off", "value"), - &Terminal::set_blink_time_off); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "blink_time_off"), - "set_blink_time_off", "get_blink_time_off"); - ClassDB::bind_method(D_METHOD("set_blink_time_on", "value"), - &Terminal::set_blink_time_on); - ClassDB::bind_method(D_METHOD("get_blink_time_on"), - &Terminal::get_blink_time_on); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "blink_time_on"), - "set_blink_time_on", "get_blink_time_on"); - - // Methods. - ClassDB::bind_method(D_METHOD("clear"), &Terminal::clear); - ClassDB::bind_method(D_METHOD("copy_all"), &Terminal::copy_all); - ClassDB::bind_method(D_METHOD("copy_selection"), &Terminal::copy_selection); - ClassDB::bind_method(D_METHOD("get_cols"), &Terminal::get_cols); - ClassDB::bind_method(D_METHOD("get_rows"), &Terminal::get_rows); - ClassDB::bind_method(D_METHOD("write", "data"), &Terminal::write); - - // Signals. - ADD_SIGNAL(MethodInfo("bell")); - ADD_SIGNAL(MethodInfo("data_sent", - PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"))); - ADD_SIGNAL(MethodInfo("key_pressed", - PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"), - PropertyInfo(Variant::OBJECT, "event"))); - ADD_SIGNAL( - MethodInfo("size_changed", PropertyInfo(Variant::VECTOR2, "new_size"))); - - // Enumerations. - BIND_ENUM_CONSTANT(UPDATE_MODE_DISABLED); - BIND_ENUM_CONSTANT(UPDATE_MODE_AUTO); - BIND_ENUM_CONSTANT(UPDATE_MODE_ALL); - BIND_ENUM_CONSTANT(UPDATE_MODE_ALL_NEXT_FRAME); - - // Private methods (must be exposed as they are connected to signals). - ClassDB::bind_method(D_METHOD("_flush"), &Terminal::_flush); - ClassDB::bind_method(D_METHOD("_on_back_buffer_draw"), - &Terminal::_on_back_buffer_draw); - ClassDB::bind_method(D_METHOD("_on_selection_held"), - &Terminal::_on_selection_held); - ClassDB::bind_method(D_METHOD("_toggle_blink"), &Terminal::_toggle_blink); +bool Terminal::_is_valid_color_name(const String &p_name) +{ + for (const char *entry : COLOR_NAMES) + { + if (std::strcmp(p_name.utf8().get_data(), entry) == 0) + { + return true; + } + } + return false; } -void Terminal::_bell_cb(tsm_vte *vte, void *data) { - Terminal *term = static_cast(data); - - if (!term->bell_muted && term->bell_cooldown == 0 || - term->bell_timer->get_time_left() == 0) { - term->emit_signal("bell"); - if (term->bell_cooldown > 0) - term->bell_timer->start(term->bell_cooldown); - } +bool Terminal::_is_valid_font_type(const String &p_name) +{ + for (const char *entry : FONT_TYPES) + { + if (std::strcmp(p_name.utf8().get_data(), entry) == 0) + { + return true; + } + } + return false; } -int Terminal::_text_draw_cb(tsm_screen *con, uint64_t id, const uint32_t *ch, - size_t len, unsigned int width, unsigned int col, - unsigned int row, const tsm_screen_attr *attr, - tsm_age_t age, void *data) { - Terminal *term = static_cast(data); - if (term->update_mode == Terminal::UpdateMode::AUTO && age != 0 && - age <= term->framebuffer_age) { - return 0; - } +void Terminal::update_sizes(bool force) +{ + Vector2 prev_size = Vector2(size); + int32_t prev_font_size = font_size; + Vector2 prev_cell_size = Vector2(cell_size); + uint prev_cols = cols; + uint prev_rows = rows; - if (width < 1) { // No foreground or background to draw. - return 0; - } + size = get_size(); + font_size = get_theme_font_size("font_size"); + cell_size = get_theme_font("normal_font")->get_string_size( + "W", HORIZONTAL_ALIGNMENT_LEFT, -1, get_theme_font_size("font_size")); + cols = floor(size.x / cell_size.x); + rows = floor(size.y / cell_size.y); - ColorPair color_pair = term->_get_cell_colors(attr); - term->_draw_background(row, col, color_pair.first, width); + if (!force && size == prev_size && font_size == prev_font_size && cell_size == prev_cell_size && cols == prev_cols && rows == prev_rows) + return; - if (len < 1) // No foreground to draw. - return 0; + tsm_screen_resize(screen, cols, rows); + rs->viewport_set_size(viewport, size.x, size.y); - size_t ulen = 0; - char buf[5] = {0}; + back_image = Image::create(std::max(cols, 1u), std::max(rows, 1u), false, Image::FORMAT_RGBA8); + back_texture->set_image(back_image); - char *utf8 = tsm_ucs4_to_utf8_alloc(ch, len, &ulen); - memcpy(buf, utf8, ulen); - term->_draw_foreground(row, col, buf, attr, color_pair.second); + attr_image = Image::create(std::max(cols, 1u), std::max(rows, 1u), false, Image::FORMAT_L8); + attr_texture->set_image(attr_image); - return 0; + back_material->set_shader_parameter("cols", cols); + back_material->set_shader_parameter("rows", rows); + back_material->set_shader_parameter("size", size); + back_material->set_shader_parameter("cell_size", cell_size); + + refresh(); } -void Terminal::_write_cb(tsm_vte *vte, const char *u8, size_t len, void *data) { - Terminal *term = static_cast(data); +void Terminal::initialize_rendering() { + ResourceLoader* rl = ResourceLoader::get_singleton(); - PackedByteArray bytes; - bytes.resize(len); - { memcpy(bytes.ptrw(), u8, len); } + rs = RenderingServer::get_singleton(); + attr_texture.instantiate(); - if (len > 0) { - if (term->last_input_event_key.is_valid()) { - // The callback was fired from a key press event so emit the "key_pressed" - // signal. - term->emit_signal("key_pressed", bytes.get_string_from_utf8(), - term->last_input_event_key); - term->last_input_event_key.unref(); - } + // Background. - term->emit_signal("data_sent", bytes); - } + back_texture.instantiate(); + + back_shader = rl->load(BACKGROUND_SHADER_PATH); + + back_material.instantiate(); + back_material->set_shader(back_shader); + back_material->set_shader_parameter("cell_colors", back_texture); + back_material->set_shader_parameter("attributes", attr_texture); + + back_canvas_item = rs->canvas_item_create(); + rs->canvas_item_set_material(back_canvas_item, back_material->get_rid()); + rs->canvas_item_set_parent(back_canvas_item, get_canvas_item()); + rs->canvas_item_set_draw_behind_parent(back_canvas_item, true); + + // Foreground. + + char_shader = rs->shader_create(); + rs->shader_set_code(char_shader, String(R"( + shader_type canvas_item; + render_mode blend_disabled; + )")); + + char_material = rs->material_create(); + rs->material_set_shader(char_material, char_shader); + + char_canvas_item = rs->canvas_item_create(); + rs->canvas_item_set_material(char_canvas_item, char_material); + + canvas = rs->canvas_create(); + rs->canvas_item_set_parent(char_canvas_item, canvas); + + viewport = rs-> viewport_create(); + rs->viewport_attach_canvas(viewport, canvas); + rs->viewport_set_disable_3d(viewport, true); + rs->viewport_set_transparent_background(viewport, true); + rs->viewport_set_clear_mode(viewport, RenderingServer::ViewportClearMode::VIEWPORT_CLEAR_NEVER); + rs->viewport_set_update_mode(viewport, RenderingServer::ViewportUpdateMode::VIEWPORT_UPDATE_WHEN_VISIBLE); + rs->viewport_set_active(viewport, true); + + fore_shader = rl->load(FOREGROUND_SHADER_PATH); + + fore_material.instantiate(); + fore_material->set_shader(fore_shader); + fore_material->set_shader_parameter("attributes", attr_texture); + + fore_canvas_item = rs->canvas_item_create(); + rs->canvas_item_set_material(fore_canvas_item, fore_material->get_rid()); + rs->canvas_item_set_parent(fore_canvas_item, get_canvas_item()); } -void Terminal::_draw_background(int row, int col, Color bgcolor, - int width = 1) { - /* Draw the background */ - Vector2 background_pos = Vector2(col * cell_size.x, row * cell_size.y); - Rect2 background_rect = Rect2(background_pos, cell_size * Vector2(width, 1)); - back_buffer->draw_rect(background_rect, bgcolor); +void Terminal::update_theme() { + palette.resize(TSM_COLOR_NUM); + for (int i = 0; i < TSM_COLOR_NUM; i++) { + tsm_vte_color color = static_cast(i); + palette[color] = get_theme_color(String(COLOR_NAMES[i])); + } + back_material->set_shader_parameter("background", palette[TSM_COLOR_BACKGROUND]); + + // TODO: Default to mono font and handle other styles. + font = get_theme_font("normal_font"); + + refresh(); } -void Terminal::_draw_foreground(int row, int col, char *ch, - const tsm_screen_attr *attr, Color fgcolor) { - Ref font; +void Terminal::draw_screen() { + if (framebuffer_age == 0) { + Rect2 rect = Rect2(Vector2(), size); - if (attr->bold && attr->italic) { - font = theme_cache.fonts["bold_italics_font"]; - } else if (attr->bold) { - font = theme_cache.fonts["bold_font"]; - } else if (attr->italic) { - font = theme_cache.fonts["italics_font"]; - } else { - font = theme_cache.fonts["normal_font"]; - } + rs->viewport_set_clear_mode(viewport, RenderingServer::ViewportClearMode::VIEWPORT_CLEAR_ONLY_NEXT_FRAME); - if (attr->blink && blink_enabled) { - if (blink_timer->is_stopped()) - blink_timer->start(blink_on ? blink_time_on : blink_time_off); + rs->canvas_item_clear(back_canvas_item); + rs->canvas_item_add_rect(back_canvas_item, rect, palette[TSM_COLOR_BLACK]); + back_image->fill(palette[TSM_COLOR_BLACK]); - if (!blink_on) - return; - } + rs->canvas_item_clear(fore_canvas_item); + rs->canvas_item_add_texture_rect(fore_canvas_item, rect, rs->viewport_get_texture(viewport)); + } - int font_height = font->get_height(theme_cache.font_size); - Vector2 foreground_pos = - Vector2(col * cell_size.x, row * cell_size.y + font_height / 1.25); - back_buffer->draw_string(font, foreground_pos, ch, HORIZONTAL_ALIGNMENT_LEFT, - -1, theme_cache.font_size, fgcolor); - - if (attr->underline) - back_buffer->draw_string(font, foreground_pos, "_", - HORIZONTAL_ALIGNMENT_LEFT, -1, - theme_cache.font_size, fgcolor); + rs->canvas_item_clear(char_canvas_item); + framebuffer_age = tsm_screen_draw(screen, Terminal::_draw_cb, this); + attr_texture->update(attr_image); + back_texture->update(back_image); } -Terminal::ColorPair Terminal::_get_cell_colors(const tsm_screen_attr *attr) { - Color fgcol, bgcol; - int8_t fccode = attr->fccode; - int8_t bccode = attr->bccode; - - // Get foreground color. - if (fccode && palette.count(fccode)) { - fgcol = palette[fccode]; - } else { - fgcol = Color(attr->fr / 255.0f, attr->fg / 255.0f, attr->fb / 255.0f); - - if (fccode != -1) - palette.insert({fccode, fgcol}); - } - - // Get background color. - if (bccode && palette.count(bccode)) { - bgcol = palette[bccode]; - } else { - bgcol = Color(attr->br / 255.0f, attr->bg / 255.0f, attr->bb / 255.0f); - - if (bccode != -1) - palette.insert({bccode, bgcol}); - } - - if (attr->inverse) - std::swap(bgcol, fgcol); - - return std::make_pair(bgcol, fgcol); +void Terminal::refresh() { + framebuffer_age = 0; + queue_redraw(); } -void Terminal::_handle_key_input(Ref event) { - if (!event.is_valid() || !event->is_pressed()) - return; +void Terminal::cleanup_rendering() { + // Background. + rs->free_rid(back_canvas_item); - const Key keycode = event->get_keycode(); - char32_t unicode = event->get_unicode(); - uint32_t ascii = unicode <= 127 ? unicode : 0; - - unsigned int mods = 0; - if (event->is_alt_pressed()) - mods |= TSM_ALT_MASK; - if (event->is_ctrl_pressed()) - mods |= TSM_CONTROL_MASK; - if (event->is_shift_pressed()) - mods |= TSM_SHIFT_MASK; - - std::pair key = {keycode, unicode}; - uint32_t keysym = - (KEY_MAP.count(key) > 0) ? KEY_MAP.at(key) : XKB_KEY_NoSymbol; - - last_input_event_key = event; - tsm_vte_handle_keyboard(vte, keysym, ascii, mods, - unicode ? unicode : TSM_VTE_INVALID); - - // Return to the bottom of the scrollback buffer if we scrolled up. Ignore - // modifier keys pressed in isolation or if Ctrl+Shift modifier keys are - // pressed. - std::set mod_keys = {KEY_ALT, KEY_SHIFT, KEY_CTRL, KEY_META}; - if (mod_keys.find(keycode) == mod_keys.end() && - !(event->is_ctrl_pressed() && event->is_shift_pressed())) { - tsm_screen_sb_reset(screen); - back_buffer->queue_redraw(); - } - - // Prevent focus changing to other inputs when pressing Tab or Arrow keys. - std::set tab_arrow_keys = {KEY_LEFT, KEY_UP, KEY_RIGHT, KEY_DOWN, - KEY_TAB}; - if (tab_arrow_keys.find(keycode) != tab_arrow_keys.end()) - accept_event(); -} - -void Terminal::_handle_mouse_wheel(Ref event) { - if (!event.is_valid() || !event->is_pressed()) - return; - - void (*scroll_func)(tsm_screen *, unsigned int) = nullptr; - - switch (event->get_button_index()) { - case MOUSE_BUTTON_WHEEL_UP: - scroll_func = &tsm_screen_sb_up; - break; - case MOUSE_BUTTON_WHEEL_DOWN: - scroll_func = &tsm_screen_sb_down; - break; - }; - - if (scroll_func != nullptr) { - // Scroll 5 times as fast as normal if alt is pressed (like TextEdit). - // Otherwise, just scroll 3 lines. - int speed = event->is_alt_pressed() ? 15 : 3; - double factor = event->get_factor(); - (*scroll_func)(screen, speed * factor); - back_buffer->queue_redraw(); - } -} - -void Terminal::_handle_selection(Ref event) { - if (!event.is_valid()) - return; - - Ref mb = event; - if (mb.is_valid()) { - if (!mb->is_pressed() || !mb->get_button_index() == MOUSE_BUTTON_LEFT) - return; - - if (selecting) { - selecting = false; - selection_mode = SelectionMode::NONE; - tsm_screen_selection_reset(screen); - back_buffer->queue_redraw(); - } - - selecting = false; - selection_mode = SelectionMode::POINTER; - - return; - } - - Ref mm = event; - if (mm.is_valid()) { - if ((mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT) && - selection_mode != SelectionMode::NONE && !selecting) { - selecting = true; - Vector2 start = event->get_position() / cell_size; - tsm_screen_selection_start(screen, start.x, start.y); - back_buffer->queue_redraw(); - selection_timer->start(); - } - return; - } -} - -void Terminal::_recalculate_size() { - Vector2 size = get_size(); - - cell_size = theme_cache.fonts["normal_font"]->get_string_size( - "W", HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size); - - rows = std::max(1, (int)floor(size.y / cell_size.y)); - cols = std::max(1, (int)floor(size.x / cell_size.x)); - - tsm_screen_resize(screen, cols, rows); - sub_viewport->set_size(size); - - emit_signal("size_changed", Vector2(cols, rows)); -} - -void Terminal::_refresh() { - back_buffer->queue_redraw(); - front_buffer->queue_redraw(); - - if (update_mode == UpdateMode::AUTO) - update_mode = UpdateMode::ALL_NEXT_FRAME; -} - -void Terminal::_update_theme_item_cache() { - // Fonts. - for (std::map::const_iterator iter = - Terminal::FONTS.begin(); - iter != Terminal::FONTS.end(); ++iter) { - String name = iter->first; - - Ref font = has_theme_font_override(name) ? get_theme_font(name) - : has_theme_font(name, "Terminal") - ? get_theme_font(name, "Terminal") - : ThemeDB::get_singleton()->get_fallback_font(); - - theme_cache.fonts[name] = font; - } - - // Font size. - theme_cache.font_size = - has_theme_font_size_override("font_size") - ? get_theme_font_size("font_size") - : has_theme_font_size("font_size", "Terminal") - ? get_theme_font_size("font_size", "Terminal") - : ThemeDB::get_singleton()->get_fallback_font_size(); - - // Colors. - uint8_t custom_palette[TSM_COLOR_NUM][3]; - - for (ColorMap::const_iterator iter = Terminal::COLORS.begin(); - iter != Terminal::COLORS.end(); ++iter) { - String name = iter->first; - - Color color = has_theme_color_override(name) ? get_theme_color(name) - : has_theme_color(name, "Terminal") - ? get_theme_color(name, "Terminal") - : color = Color::html(iter->second.default_color); - - theme_cache.colors[name] = color; - palette[iter->second.tsm_color] = color; - custom_palette[iter->second.tsm_color][0] = color.get_r8(); - custom_palette[iter->second.tsm_color][1] = color.get_g8(); - custom_palette[iter->second.tsm_color][2] = color.get_b8(); - } - - if (tsm_vte_set_custom_palette(vte, custom_palette)) - ERR_PRINT("Error setting custom palette."); - if (tsm_vte_set_palette(vte, "custom")) - ERR_PRINT("Error setting palette to custom palette."); - - _recalculate_size(); + // Foreground. + rs->free_rid(viewport); + rs->free_rid(canvas); + rs->free_rid(char_canvas_item); + rs->free_rid(char_material); + rs->free_rid(char_shader); } diff --git a/addons/godot_xterm/native/src/terminal.h b/addons/godot_xterm/native/src/terminal.h index d893818..4a857b4 100644 --- a/addons/godot_xterm/native/src/terminal.h +++ b/addons/godot_xterm/native/src/terminal.h @@ -1,164 +1,110 @@ -// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson +// SPDX-FileCopyrightText: 2021-2024 Leroy Hopson // SPDX-License-Identifier: MIT -#ifndef GODOT_XTERM_TERMINAL_H -#define GODOT_XTERM_TERMINAL_H +#pragma once #include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include #include -#include -#include -using namespace godot; +namespace godot +{ -class Terminal : public Control { - GDCLASS(Terminal, Control) + class Terminal : public Control + { + GDCLASS(Terminal, Control) -public: - Terminal(); - ~Terminal(); + private: + static constexpr const char *COLOR_NAMES[] = { + "ansi_0_color", "ansi_1_color", "ansi_2_color", "ansi_3_color", "ansi_4_color", "ansi_5_color", "ansi_6_color", "ansi_7_color", + "ansi_8_color", "ansi_9_color", "ansi_10_color", "ansi_11_color", "ansi_12_color", "ansi_13_color", "ansi_14_color", "ansi_15_color", + "foreground_color", "background_color", + }; - enum UpdateMode { - DISABLED, - AUTO, - ALL, - ALL_NEXT_FRAME, + static constexpr const char *FONT_TYPES[] = { + "normal_font", "bold_font", "italics_font", "bold_italics_font", + }; + + public: + enum AttrFlags + { + INVERSE = 1 << 0, + BLINK = 1 << 1, + }; + + Terminal(); + ~Terminal(); + + void set_cols(const int p_cols); + int get_cols() const; + + void set_rows(const int p_rows); + int get_rows() const; + + void set_max_scrollback(const int p_max_scrollback); + int get_max_scrollback() const; + + void write(Variant data); + + protected: + static void _bind_methods(); + + private: + unsigned int max_scrollback; + + unsigned int cols; + unsigned int rows; + + RenderingServer *rs; + + tsm_screen *screen; + tsm_vte *vte; + tsm_age_t framebuffer_age; + + static void _write_cb(struct tsm_vte *vte, const char *u8, size_t len, + void *data); + static int _draw_cb(struct tsm_screen *con, uint64_t id, const uint32_t *ch, + size_t len, unsigned int width, unsigned int posx, + unsigned int posy, const struct tsm_screen_attr *attr, + tsm_age_t age, void *data); + + PackedColorArray palette; + Ref font; + int32_t font_size; + Vector2 size; + Vector2 cell_size; + + Ref attr_image; + Ref attr_texture; + + // Background. + Ref back_image; + Ref back_texture; + Ref back_shader; + Ref back_material; + RID back_canvas_item; + + // Foreground. + RID char_shader, char_material, char_canvas_item, canvas, viewport, + fore_canvas_item; + Ref fore_shader; + Ref fore_material; + + void _notification(const int what); + + void initialize_rendering(); + void update_theme(); + void update_sizes(bool force = false); + void draw_screen(); + void refresh(); + void cleanup_rendering(); + + bool _set(const StringName &p_name, const Variant &p_value); + void _get_property_list(List *p_list) const; + bool _is_valid_color_name(const String &p_name); + bool _is_valid_font_type(const String &p_name); }; - static const UpdateMode UPDATE_MODE_DISABLED = UpdateMode::DISABLED; - static const UpdateMode UPDATE_MODE_AUTO = UpdateMode::AUTO; - static const UpdateMode UPDATE_MODE_ALL = UpdateMode::ALL; - static const UpdateMode UPDATE_MODE_ALL_NEXT_FRAME = - UpdateMode::ALL_NEXT_FRAME; - - bool copy_on_selection = false; - void set_copy_on_selection(bool value); - bool get_copy_on_selection(); - - UpdateMode update_mode = UPDATE_MODE_AUTO; - void set_update_mode(UpdateMode value); - UpdateMode get_update_mode(); - - double bell_cooldown = 0.1f; - void set_bell_cooldown(double value); - double get_bell_cooldown(); - - bool bell_muted = false; - void set_bell_muted(bool value); - bool get_bell_muted(); - - bool blink_enabled = true; - void set_blink_enabled(bool value); - bool get_blink_enabled(); - - double blink_time_off = 0.3; - void set_blink_time_off(double value); - double get_blink_time_off(); - - double blink_time_on = 0.6; - void set_blink_time_on(double value); - double get_blink_time_on(); - - void clear(); - String copy_all(); - String copy_selection(); - int get_cols(); - int get_rows(); - void write(Variant data); - - void _gui_input(Ref event); - void _notification(int what); - - void _flush(); - void _on_back_buffer_draw(); - void _on_selection_held(); - void _toggle_blink(); - -protected: - static void _bind_methods(); - -private: - struct ColorDef { - const char *default_color; - tsm_vte_color tsm_color; - }; - - struct ThemeCache { - int font_size = 0; - std::map> fonts = std::map>{}; - std::map colors = std::map{}; - } theme_cache; - - typedef std::map ColorMap; - typedef std::pair ColorPair; - typedef std::map FontMap; - typedef std::map, uint32_t> KeyMap; - - static const KeyMap KEY_MAP; - static const ColorMap COLORS; - static const FontMap FONTS; - - enum SelectionMode { NONE, POINTER }; - - Control *back_buffer = new Control(); - SubViewport *sub_viewport = new SubViewport(); - TextureRect *front_buffer = new TextureRect(); - - Timer *bell_timer = new Timer(); - Timer *blink_timer = new Timer(); - Timer *selection_timer = new Timer(); - - tsm_screen *screen; - tsm_vte *vte; - - Array write_buffer = Array(); - - int cols = 80; - int rows = 24; - - // Whether blinking characters are visible. Not whether blinking is enabled - // which is determined by `blink_enabled`. - bool blink_on = true; - - Vector2 cell_size = Vector2(0, 0); - - std::map palette = {}; - - tsm_age_t framebuffer_age; - - Ref last_input_event_key; - - bool selecting = false; - SelectionMode selection_mode = SelectionMode::NONE; - - static void _bell_cb(tsm_vte *vte, void *data); - static int _text_draw_cb(tsm_screen *con, uint64_t id, const uint32_t *ch, - size_t len, unsigned int width, unsigned int col, - unsigned int row, const tsm_screen_attr *attr, - tsm_age_t age, void *data); - static void _write_cb(tsm_vte *vte, const char *u8, size_t len, void *data); - - void _draw_background(int row, int col, Color bgcol, int width); - void _draw_foreground(int row, int col, char *ch, const tsm_screen_attr *attr, - Color fgcol); - ColorPair _get_cell_colors(const tsm_screen_attr *attr); - void _handle_key_input(Ref event); - void _handle_mouse_wheel(Ref event); - void _handle_selection(Ref event); - void _recalculate_size(); - void _refresh(); - void _update_theme_item_cache(); -}; - -VARIANT_ENUM_CAST(Terminal::UpdateMode); - -#endif // GODOT_XTERM_TERMINAL_H +} // namespace godot diff --git a/addons/godot_xterm/native/src/.gdignore b/addons/godot_xterm/native/src_old/.gdignore similarity index 100% rename from addons/godot_xterm/native/src/.gdignore rename to addons/godot_xterm/native/src_old/.gdignore diff --git a/addons/godot_xterm/native/src/constants.cpp b/addons/godot_xterm/native/src_old/constants.cpp similarity index 100% rename from addons/godot_xterm/native/src/constants.cpp rename to addons/godot_xterm/native/src_old/constants.cpp diff --git a/addons/godot_xterm/native/src/libuv_utils.cpp b/addons/godot_xterm/native/src_old/libuv_utils.cpp similarity index 100% rename from addons/godot_xterm/native/src/libuv_utils.cpp rename to addons/godot_xterm/native/src_old/libuv_utils.cpp diff --git a/addons/godot_xterm/native/src/libuv_utils.h b/addons/godot_xterm/native/src_old/libuv_utils.h similarity index 100% rename from addons/godot_xterm/native/src/libuv_utils.h rename to addons/godot_xterm/native/src_old/libuv_utils.h diff --git a/addons/godot_xterm/native/src/node_pty/LICENSE.md b/addons/godot_xterm/native/src_old/node_pty/LICENSE.md similarity index 100% rename from addons/godot_xterm/native/src/node_pty/LICENSE.md rename to addons/godot_xterm/native/src_old/node_pty/LICENSE.md diff --git a/addons/godot_xterm/native/src/node_pty/LICENSE_node-pty b/addons/godot_xterm/native/src_old/node_pty/LICENSE_node-pty similarity index 100% rename from addons/godot_xterm/native/src/node_pty/LICENSE_node-pty rename to addons/godot_xterm/native/src_old/node_pty/LICENSE_node-pty diff --git a/addons/godot_xterm/native/src/node_pty/LICENSE_tmux b/addons/godot_xterm/native/src_old/node_pty/LICENSE_tmux similarity index 100% rename from addons/godot_xterm/native/src/node_pty/LICENSE_tmux rename to addons/godot_xterm/native/src_old/node_pty/LICENSE_tmux diff --git a/addons/godot_xterm/native/src/node_pty/unix/pty.cc b/addons/godot_xterm/native/src_old/node_pty/unix/pty.cc similarity index 100% rename from addons/godot_xterm/native/src/node_pty/unix/pty.cc rename to addons/godot_xterm/native/src_old/node_pty/unix/pty.cc diff --git a/addons/godot_xterm/native/src/node_pty/unix/pty.h b/addons/godot_xterm/native/src_old/node_pty/unix/pty.h similarity index 100% rename from addons/godot_xterm/native/src/node_pty/unix/pty.h rename to addons/godot_xterm/native/src_old/node_pty/unix/pty.h diff --git a/addons/godot_xterm/native/src/node_pty/win/conpty.cc b/addons/godot_xterm/native/src_old/node_pty/win/conpty.cc similarity index 100% rename from addons/godot_xterm/native/src/node_pty/win/conpty.cc rename to addons/godot_xterm/native/src_old/node_pty/win/conpty.cc diff --git a/addons/godot_xterm/native/src/node_pty/win/conpty.h b/addons/godot_xterm/native/src_old/node_pty/win/conpty.h similarity index 100% rename from addons/godot_xterm/native/src/node_pty/win/conpty.h rename to addons/godot_xterm/native/src_old/node_pty/win/conpty.h diff --git a/addons/godot_xterm/native/src/node_pty/win/conpty_console_list.cc b/addons/godot_xterm/native/src_old/node_pty/win/conpty_console_list.cc similarity index 100% rename from addons/godot_xterm/native/src/node_pty/win/conpty_console_list.cc rename to addons/godot_xterm/native/src_old/node_pty/win/conpty_console_list.cc diff --git a/addons/godot_xterm/native/src/node_pty/win/path_util.cc b/addons/godot_xterm/native/src_old/node_pty/win/path_util.cc similarity index 100% rename from addons/godot_xterm/native/src/node_pty/win/path_util.cc rename to addons/godot_xterm/native/src_old/node_pty/win/path_util.cc diff --git a/addons/godot_xterm/native/src/node_pty/win/path_util.h b/addons/godot_xterm/native/src_old/node_pty/win/path_util.h similarity index 100% rename from addons/godot_xterm/native/src/node_pty/win/path_util.h rename to addons/godot_xterm/native/src_old/node_pty/win/path_util.h diff --git a/addons/godot_xterm/native/src/node_pty/win/winpty.cc b/addons/godot_xterm/native/src_old/node_pty/win/winpty.cc similarity index 100% rename from addons/godot_xterm/native/src/node_pty/win/winpty.cc rename to addons/godot_xterm/native/src_old/node_pty/win/winpty.cc diff --git a/addons/godot_xterm/native/src/pipe.cpp b/addons/godot_xterm/native/src_old/pipe.cpp similarity index 100% rename from addons/godot_xterm/native/src/pipe.cpp rename to addons/godot_xterm/native/src_old/pipe.cpp diff --git a/addons/godot_xterm/native/src/pipe.h b/addons/godot_xterm/native/src_old/pipe.h similarity index 100% rename from addons/godot_xterm/native/src/pipe.h rename to addons/godot_xterm/native/src_old/pipe.h diff --git a/addons/godot_xterm/native/src_old/terminal.cpp b/addons/godot_xterm/native/src_old/terminal.cpp new file mode 100644 index 0000000..0957089 --- /dev/null +++ b/addons/godot_xterm/native/src_old/terminal.cpp @@ -0,0 +1,654 @@ +// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson +// SPDX-License-Identifier: MIT + +#include "terminal.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define UNICODE_MAX 0x10FFFF + +using namespace godot; + +Terminal::Terminal() { + // Ensure we write to terminal before the frame is drawn. Otherwise, the + // terminal state may be updated but not drawn until it is updated again, + // which may not happen for some time. + RenderingServer::get_singleton()->connect("frame_pre_draw", + Callable(this, "_flush")); + + // Override default focus mode. + set_focus_mode(FOCUS_ALL); + + // Name our nodes for easier debugging. + back_buffer->set_name("BackBuffer"); + sub_viewport->set_name("SubViewport"); + front_buffer->set_name("FrontBuffer"); + + // Ensure buffers always have correct size. + back_buffer->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT); + front_buffer->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT); + + // Setup back buffer. + back_buffer->connect("draw", Callable(this, "_on_back_buffer_draw")); + + // Setup sub viewport. + sub_viewport->set_handle_input_locally(false); + sub_viewport->set_transparent_background(true); + sub_viewport->set_snap_controls_to_pixels(false); + sub_viewport->set_update_mode(SubViewport::UPDATE_WHEN_PARENT_VISIBLE); + sub_viewport->set_clear_mode(SubViewport::CLEAR_MODE_NEVER); + sub_viewport->add_child(back_buffer); + add_child(sub_viewport); + + // Setup bell timer. + bell_timer->set_name("BellTimer"); + bell_timer->set_one_shot(true); + add_child(bell_timer); + + // Setup blink timer. + blink_timer->set_name("BlinkTimer"); + blink_timer->set_one_shot(true); + blink_timer->connect("timeout", Callable(this, "_toggle_blink")); + add_child(blink_timer); + + // Setup selection timer. + selection_timer->set_name("SelectionTimer"); + selection_timer->set_wait_time(0.05); + selection_timer->connect("timeout", Callable(this, "_on_selection_held")); + add_child(selection_timer); + + // Setup front buffer. + front_buffer->set_texture(sub_viewport->get_texture()); + add_child(front_buffer); + + framebuffer_age = 0; + update_mode = UpdateMode::AUTO; + + if (tsm_screen_new(&screen, NULL, NULL)) { + ERR_PRINT("Error creating new tsm screen."); + } + tsm_screen_set_max_sb(screen, 1000); + + if (tsm_vte_new(&vte, screen, &Terminal::_write_cb, this, NULL, NULL)) { + ERR_PRINT("Error creating new tsm vte."); + } + + tsm_vte_set_bell_cb(vte, &Terminal::_bell_cb, this); + + _update_theme_item_cache(); +} + +Terminal::~Terminal() { + back_buffer->queue_free(); + sub_viewport->queue_free(); + front_buffer->queue_free(); +} + +void Terminal::set_copy_on_selection(bool value) { copy_on_selection = value; } + +bool Terminal::get_copy_on_selection() { return copy_on_selection; } + +void Terminal::set_update_mode(Terminal::UpdateMode value) { + update_mode = value; +}; + +Terminal::UpdateMode Terminal::get_update_mode() { return update_mode; } + +void Terminal::set_bell_cooldown(double value) { bell_cooldown = value; } + +double Terminal::get_bell_cooldown() { return bell_cooldown; } + +void Terminal::set_bell_muted(bool value) { bell_muted = value; } + +bool Terminal::get_bell_muted() { return bell_muted; } + +void Terminal::set_blink_enabled(bool value) { + blink_enabled = value; + + if (!blink_enabled) + blink_timer->stop(); + + _refresh(); +} + +bool Terminal::get_blink_enabled() { return blink_enabled; } + +void Terminal::set_blink_time_off(double value) { + blink_time_off = value; + + if (!blink_on && !blink_timer->is_stopped()) { + double time_left = blink_timer->get_time_left(); + blink_timer->start(std::min(blink_time_off, time_left)); + } +} + +double Terminal::get_blink_time_off() { return blink_time_off; } + +void Terminal::set_blink_time_on(double value) { + blink_time_on = value; + + if (blink_on && !blink_timer->is_stopped()) { + double time_left = blink_timer->get_time_left(); + blink_timer->start(std::min(blink_time_on, time_left)); + } +} + +double Terminal::get_blink_time_on() { return blink_time_on; } + +void Terminal::clear() { + Vector2 initial_size = get_size(); + set_size(Vector2(initial_size.x, cell_size.y)); + tsm_screen_clear_sb(screen); + set_size(initial_size); + back_buffer->queue_redraw(); +} + +String Terminal::copy_all() { + char *out = nullptr; + int len = tsm_screen_copy_all(screen, &out); + String result = String(out); + std::free(out); + return result; +} + +String Terminal::copy_selection() { + char *out = nullptr; + int len = tsm_screen_selection_copy(screen, &out); + String result = String(out); + std::free(out); + return result; +} + +int Terminal::get_cols() { return cols; } +int Terminal::get_rows() { return rows; } + +void Terminal::write(Variant data) { + switch (data.get_type()) { + case Variant::PACKED_BYTE_ARRAY: + break; + case Variant::STRING: + data = ((String)data).to_utf8_buffer(); + break; + default: + ERR_PRINT("Expected data to be a String or PackedByteArray."); + } + + write_buffer.push_back(data); + + queue_redraw(); +} + +void Terminal::_gui_input(Ref event) { + _handle_key_input(event); + _handle_selection(event); + _handle_mouse_wheel(event); +} + +void Terminal::_notification(int what) { + switch (what) { + case NOTIFICATION_RESIZED: + _recalculate_size(); + sub_viewport->set_size(get_size()); + _refresh(); + break; + case NOTIFICATION_THEME_CHANGED: + _update_theme_item_cache(); + _refresh(); + break; + } +} + +void Terminal::_flush() { + if (write_buffer.is_empty()) + return; + + for (int i = 0; i < write_buffer.size(); i++) { + PackedByteArray data = static_cast(write_buffer[i]); + tsm_vte_input(vte, (char *)data.ptr(), data.size()); + } + + write_buffer.clear(); + + back_buffer->queue_redraw(); +} + +void Terminal::_on_back_buffer_draw() { + if (update_mode == UpdateMode::DISABLED) { + return; + } + + if ((update_mode > UpdateMode::AUTO) || framebuffer_age == 0) { + Color background_color = palette[TSM_COLOR_BACKGROUND]; + back_buffer->draw_rect(back_buffer->get_rect(), background_color); + } + + int prev_framebuffer_age = framebuffer_age; + framebuffer_age = tsm_screen_draw(screen, &Terminal::_text_draw_cb, this); + + if (update_mode == UpdateMode::ALL_NEXT_FRAME && prev_framebuffer_age != 0) + update_mode = UpdateMode::AUTO; +} + +void Terminal::_on_selection_held() { + if (!(Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) || + selection_mode == SelectionMode::NONE) { + if (copy_on_selection) + DisplayServer::get_singleton()->clipboard_set_primary(copy_selection()); + selection_timer->stop(); + return; + } + + Vector2 target = get_local_mouse_position() / cell_size; + tsm_screen_selection_target(screen, target.x, target.y); + back_buffer->queue_redraw(); + selection_timer->start(); +} + +void Terminal::_toggle_blink() { + if (blink_enabled) { + blink_on = !blink_on; + _refresh(); + } +} + +void Terminal::_bind_methods() { + // Properties. + ClassDB::bind_method(D_METHOD("set_copy_on_selection", "value"), + &Terminal::set_copy_on_selection); + ClassDB::bind_method(D_METHOD("get_copy_on_selection"), + &Terminal::get_copy_on_selection); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "copy_on_selection"), + "set_copy_on_selection", "get_copy_on_selection"); + ClassDB::bind_method(D_METHOD("set_update_mode", "value"), + &Terminal::set_update_mode); + ClassDB::bind_method(D_METHOD("get_update_mode"), &Terminal::get_update_mode); + ADD_PROPERTY(PropertyInfo(Variant::INT, "update_mode", PROPERTY_HINT_ENUM, + "Disabled,Auto,All,All Next Frame"), + "set_update_mode", "get_update_mode"); + + ADD_GROUP("Bell", "bell_"); + ClassDB::bind_method(D_METHOD("set_bell_cooldown", "value"), + &Terminal::set_bell_cooldown); + ClassDB::bind_method(D_METHOD("get_bell_cooldown"), + &Terminal::get_bell_cooldown); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bell_cooldown"), + "set_bell_cooldown", "get_bell_cooldown"); + ClassDB::bind_method(D_METHOD("set_bell_muted", "value"), + &Terminal::set_bell_muted); + ClassDB::bind_method(D_METHOD("get_bell_muted"), &Terminal::get_bell_muted); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "bell_muted"), "set_bell_muted", + "get_bell_muted"); + + ADD_GROUP("Blink", "blink_"); + ClassDB::bind_method(D_METHOD("set_blink_enabled", "value"), + &Terminal::set_blink_enabled); + ClassDB::bind_method(D_METHOD("get_blink_enabled"), + &Terminal::get_blink_enabled); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "blink_enabled"), + "set_blink_enabled", "get_blink_enabled"); + ClassDB::bind_method(D_METHOD("get_blink_time_off"), + &Terminal::get_blink_time_off); + ClassDB::bind_method(D_METHOD("set_blink_time_off", "value"), + &Terminal::set_blink_time_off); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "blink_time_off"), + "set_blink_time_off", "get_blink_time_off"); + ClassDB::bind_method(D_METHOD("set_blink_time_on", "value"), + &Terminal::set_blink_time_on); + ClassDB::bind_method(D_METHOD("get_blink_time_on"), + &Terminal::get_blink_time_on); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "blink_time_on"), + "set_blink_time_on", "get_blink_time_on"); + + // Methods. + ClassDB::bind_method(D_METHOD("clear"), &Terminal::clear); + ClassDB::bind_method(D_METHOD("copy_all"), &Terminal::copy_all); + ClassDB::bind_method(D_METHOD("copy_selection"), &Terminal::copy_selection); + ClassDB::bind_method(D_METHOD("get_cols"), &Terminal::get_cols); + ClassDB::bind_method(D_METHOD("get_rows"), &Terminal::get_rows); + ClassDB::bind_method(D_METHOD("write", "data"), &Terminal::write); + + // Signals. + ADD_SIGNAL(MethodInfo("bell")); + ADD_SIGNAL(MethodInfo("data_sent", + PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"))); + ADD_SIGNAL(MethodInfo("key_pressed", + PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"), + PropertyInfo(Variant::OBJECT, "event"))); + ADD_SIGNAL( + MethodInfo("size_changed", PropertyInfo(Variant::VECTOR2, "new_size"))); + + // Enumerations. + BIND_ENUM_CONSTANT(UPDATE_MODE_DISABLED); + BIND_ENUM_CONSTANT(UPDATE_MODE_AUTO); + BIND_ENUM_CONSTANT(UPDATE_MODE_ALL); + BIND_ENUM_CONSTANT(UPDATE_MODE_ALL_NEXT_FRAME); + + // Private methods (must be exposed as they are connected to signals). + ClassDB::bind_method(D_METHOD("_flush"), &Terminal::_flush); + ClassDB::bind_method(D_METHOD("_on_back_buffer_draw"), + &Terminal::_on_back_buffer_draw); + ClassDB::bind_method(D_METHOD("_on_selection_held"), + &Terminal::_on_selection_held); + ClassDB::bind_method(D_METHOD("_toggle_blink"), &Terminal::_toggle_blink); +} + +void Terminal::_bell_cb(tsm_vte *vte, void *data) { + Terminal *term = static_cast(data); + + if (!term->bell_muted && term->bell_cooldown == 0 || + term->bell_timer->get_time_left() == 0) { + term->emit_signal("bell"); + if (term->bell_cooldown > 0) + term->bell_timer->start(term->bell_cooldown); + } +} + +int Terminal::_text_draw_cb(tsm_screen *con, uint64_t id, const uint32_t *ch, + size_t len, unsigned int width, unsigned int col, + unsigned int row, const tsm_screen_attr *attr, + tsm_age_t age, void *data) { + Terminal *term = static_cast(data); + if (term->update_mode == Terminal::UpdateMode::AUTO && age != 0 && + age <= term->framebuffer_age) { + return 0; + } + + if (width < 1) { // No foreground or background to draw. + return 0; + } + + ColorPair color_pair = term->_get_cell_colors(attr); + term->_draw_background(row, col, color_pair.first, width); + + if (len < 1) // No foreground to draw. + return 0; + + size_t ulen = 0; + char buf[5] = {0}; + + char *utf8 = tsm_ucs4_to_utf8_alloc(ch, len, &ulen); + memcpy(buf, utf8, ulen); + term->_draw_foreground(row, col, buf, attr, color_pair.second); + + return 0; +} + +void Terminal::_write_cb(tsm_vte *vte, const char *u8, size_t len, void *data) { + Terminal *term = static_cast(data); + + PackedByteArray bytes; + bytes.resize(len); + { memcpy(bytes.ptrw(), u8, len); } + + if (len > 0) { + if (term->last_input_event_key.is_valid()) { + // The callback was fired from a key press event so emit the "key_pressed" + // signal. + term->emit_signal("key_pressed", bytes.get_string_from_utf8(), + term->last_input_event_key); + term->last_input_event_key.unref(); + } + + term->emit_signal("data_sent", bytes); + } +} + +void Terminal::_draw_background(int row, int col, Color bgcolor, + int width = 1) { + /* Draw the background */ + Vector2 background_pos = Vector2(col * cell_size.x, row * cell_size.y); + Rect2 background_rect = Rect2(background_pos, cell_size * Vector2(width, 1)); + back_buffer->draw_rect(background_rect, bgcolor); +} + +void Terminal::_draw_foreground(int row, int col, char *ch, + const tsm_screen_attr *attr, Color fgcolor) { + Ref font; + + if (attr->bold && attr->italic) { + font = theme_cache.fonts["bold_italics_font"]; + } else if (attr->bold) { + font = theme_cache.fonts["bold_font"]; + } else if (attr->italic) { + font = theme_cache.fonts["italics_font"]; + } else { + font = theme_cache.fonts["normal_font"]; + } + + if (attr->blink && blink_enabled) { + if (blink_timer->is_stopped()) + blink_timer->start(blink_on ? blink_time_on : blink_time_off); + + if (!blink_on) + return; + } + + int font_height = font->get_height(theme_cache.font_size); + Vector2 foreground_pos = + Vector2(col * cell_size.x, row * cell_size.y + font_height / 1.25); + back_buffer->draw_string(font, foreground_pos, ch, HORIZONTAL_ALIGNMENT_LEFT, + -1, theme_cache.font_size, fgcolor); + + if (attr->underline) + back_buffer->draw_string(font, foreground_pos, "_", + HORIZONTAL_ALIGNMENT_LEFT, -1, + theme_cache.font_size, fgcolor); +} + +Terminal::ColorPair Terminal::_get_cell_colors(const tsm_screen_attr *attr) { + Color fgcol, bgcol; + int8_t fccode = attr->fccode; + int8_t bccode = attr->bccode; + + // Get foreground color. + if (fccode && palette.count(fccode)) { + fgcol = palette[fccode]; + } else { + fgcol = Color(attr->fr / 255.0f, attr->fg / 255.0f, attr->fb / 255.0f); + + if (fccode != -1) + palette.insert({fccode, fgcol}); + } + + // Get background color. + if (bccode && palette.count(bccode)) { + bgcol = palette[bccode]; + } else { + bgcol = Color(attr->br / 255.0f, attr->bg / 255.0f, attr->bb / 255.0f); + + if (bccode != -1) + palette.insert({bccode, bgcol}); + } + + if (attr->inverse) + std::swap(bgcol, fgcol); + + return std::make_pair(bgcol, fgcol); +} + +void Terminal::_handle_key_input(Ref event) { + if (!event.is_valid() || !event->is_pressed()) + return; + + const Key keycode = event->get_keycode(); + char32_t unicode = event->get_unicode(); + uint32_t ascii = unicode <= 127 ? unicode : 0; + + unsigned int mods = 0; + if (event->is_alt_pressed()) + mods |= TSM_ALT_MASK; + if (event->is_ctrl_pressed()) + mods |= TSM_CONTROL_MASK; + if (event->is_shift_pressed()) + mods |= TSM_SHIFT_MASK; + + std::pair key = {keycode, unicode}; + uint32_t keysym = + (KEY_MAP.count(key) > 0) ? KEY_MAP.at(key) : XKB_KEY_NoSymbol; + + last_input_event_key = event; + tsm_vte_handle_keyboard(vte, keysym, ascii, mods, + unicode ? unicode : TSM_VTE_INVALID); + + // Return to the bottom of the scrollback buffer if we scrolled up. Ignore + // modifier keys pressed in isolation or if Ctrl+Shift modifier keys are + // pressed. + std::set mod_keys = {KEY_ALT, KEY_SHIFT, KEY_CTRL, KEY_META}; + if (mod_keys.find(keycode) == mod_keys.end() && + !(event->is_ctrl_pressed() && event->is_shift_pressed())) { + tsm_screen_sb_reset(screen); + back_buffer->queue_redraw(); + } + + // Prevent focus changing to other inputs when pressing Tab or Arrow keys. + std::set tab_arrow_keys = {KEY_LEFT, KEY_UP, KEY_RIGHT, KEY_DOWN, + KEY_TAB}; + if (tab_arrow_keys.find(keycode) != tab_arrow_keys.end()) + accept_event(); +} + +void Terminal::_handle_mouse_wheel(Ref event) { + if (!event.is_valid() || !event->is_pressed()) + return; + + void (*scroll_func)(tsm_screen *, unsigned int) = nullptr; + + switch (event->get_button_index()) { + case MOUSE_BUTTON_WHEEL_UP: + scroll_func = &tsm_screen_sb_up; + break; + case MOUSE_BUTTON_WHEEL_DOWN: + scroll_func = &tsm_screen_sb_down; + break; + }; + + if (scroll_func != nullptr) { + // Scroll 5 times as fast as normal if alt is pressed (like TextEdit). + // Otherwise, just scroll 3 lines. + int speed = event->is_alt_pressed() ? 15 : 3; + double factor = event->get_factor(); + (*scroll_func)(screen, speed * factor); + back_buffer->queue_redraw(); + } +} + +void Terminal::_handle_selection(Ref event) { + if (!event.is_valid()) + return; + + Ref mb = event; + if (mb.is_valid()) { + if (!mb->is_pressed() || !mb->get_button_index() == MOUSE_BUTTON_LEFT) + return; + + if (selecting) { + selecting = false; + selection_mode = SelectionMode::NONE; + tsm_screen_selection_reset(screen); + back_buffer->queue_redraw(); + } + + selecting = false; + selection_mode = SelectionMode::POINTER; + + return; + } + + Ref mm = event; + if (mm.is_valid()) { + if ((mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT) && + selection_mode != SelectionMode::NONE && !selecting) { + selecting = true; + Vector2 start = event->get_position() / cell_size; + tsm_screen_selection_start(screen, start.x, start.y); + back_buffer->queue_redraw(); + selection_timer->start(); + } + return; + } +} + +void Terminal::_recalculate_size() { + Vector2 size = get_size(); + + cell_size = theme_cache.fonts["normal_font"]->get_string_size( + "W", HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size); + + rows = std::max(1, (int)floor(size.y / cell_size.y)); + cols = std::max(1, (int)floor(size.x / cell_size.x)); + + tsm_screen_resize(screen, cols, rows); + sub_viewport->set_size(size); + + emit_signal("size_changed", Vector2(cols, rows)); +} + +void Terminal::_refresh() { + back_buffer->queue_redraw(); + front_buffer->queue_redraw(); + + if (update_mode == UpdateMode::AUTO) + update_mode = UpdateMode::ALL_NEXT_FRAME; +} + +void Terminal::_update_theme_item_cache() { + // Fonts. + for (std::map::const_iterator iter = + Terminal::FONTS.begin(); + iter != Terminal::FONTS.end(); ++iter) { + String name = iter->first; + + Ref font = has_theme_font_override(name) ? get_theme_font(name) + : has_theme_font(name, "Terminal") + ? get_theme_font(name, "Terminal") + : ThemeDB::get_singleton()->get_fallback_font(); + + theme_cache.fonts[name] = font; + } + + // Font size. + theme_cache.font_size = + has_theme_font_size_override("font_size") + ? get_theme_font_size("font_size") + : has_theme_font_size("font_size", "Terminal") + ? get_theme_font_size("font_size", "Terminal") + : ThemeDB::get_singleton()->get_fallback_font_size(); + + // Colors. + uint8_t custom_palette[TSM_COLOR_NUM][3]; + + for (ColorMap::const_iterator iter = Terminal::COLORS.begin(); + iter != Terminal::COLORS.end(); ++iter) { + String name = iter->first; + + Color color = has_theme_color_override(name) ? get_theme_color(name) + : has_theme_color(name, "Terminal") + ? get_theme_color(name, "Terminal") + : color = Color::html(iter->second.default_color); + + theme_cache.colors[name] = color; + palette[iter->second.tsm_color] = color; + custom_palette[iter->second.tsm_color][0] = color.get_r8(); + custom_palette[iter->second.tsm_color][1] = color.get_g8(); + custom_palette[iter->second.tsm_color][2] = color.get_b8(); + } + + if (tsm_vte_set_custom_palette(vte, custom_palette)) + ERR_PRINT("Error setting custom palette."); + if (tsm_vte_set_palette(vte, "custom")) + ERR_PRINT("Error setting palette to custom palette."); + + _recalculate_size(); +} diff --git a/addons/godot_xterm/native/src_old/terminal.h b/addons/godot_xterm/native/src_old/terminal.h new file mode 100644 index 0000000..d893818 --- /dev/null +++ b/addons/godot_xterm/native/src_old/terminal.h @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson +// SPDX-License-Identifier: MIT + +#ifndef GODOT_XTERM_TERMINAL_H +#define GODOT_XTERM_TERMINAL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace godot; + +class Terminal : public Control { + GDCLASS(Terminal, Control) + +public: + Terminal(); + ~Terminal(); + + enum UpdateMode { + DISABLED, + AUTO, + ALL, + ALL_NEXT_FRAME, + }; + + static const UpdateMode UPDATE_MODE_DISABLED = UpdateMode::DISABLED; + static const UpdateMode UPDATE_MODE_AUTO = UpdateMode::AUTO; + static const UpdateMode UPDATE_MODE_ALL = UpdateMode::ALL; + static const UpdateMode UPDATE_MODE_ALL_NEXT_FRAME = + UpdateMode::ALL_NEXT_FRAME; + + bool copy_on_selection = false; + void set_copy_on_selection(bool value); + bool get_copy_on_selection(); + + UpdateMode update_mode = UPDATE_MODE_AUTO; + void set_update_mode(UpdateMode value); + UpdateMode get_update_mode(); + + double bell_cooldown = 0.1f; + void set_bell_cooldown(double value); + double get_bell_cooldown(); + + bool bell_muted = false; + void set_bell_muted(bool value); + bool get_bell_muted(); + + bool blink_enabled = true; + void set_blink_enabled(bool value); + bool get_blink_enabled(); + + double blink_time_off = 0.3; + void set_blink_time_off(double value); + double get_blink_time_off(); + + double blink_time_on = 0.6; + void set_blink_time_on(double value); + double get_blink_time_on(); + + void clear(); + String copy_all(); + String copy_selection(); + int get_cols(); + int get_rows(); + void write(Variant data); + + void _gui_input(Ref event); + void _notification(int what); + + void _flush(); + void _on_back_buffer_draw(); + void _on_selection_held(); + void _toggle_blink(); + +protected: + static void _bind_methods(); + +private: + struct ColorDef { + const char *default_color; + tsm_vte_color tsm_color; + }; + + struct ThemeCache { + int font_size = 0; + std::map> fonts = std::map>{}; + std::map colors = std::map{}; + } theme_cache; + + typedef std::map ColorMap; + typedef std::pair ColorPair; + typedef std::map FontMap; + typedef std::map, uint32_t> KeyMap; + + static const KeyMap KEY_MAP; + static const ColorMap COLORS; + static const FontMap FONTS; + + enum SelectionMode { NONE, POINTER }; + + Control *back_buffer = new Control(); + SubViewport *sub_viewport = new SubViewport(); + TextureRect *front_buffer = new TextureRect(); + + Timer *bell_timer = new Timer(); + Timer *blink_timer = new Timer(); + Timer *selection_timer = new Timer(); + + tsm_screen *screen; + tsm_vte *vte; + + Array write_buffer = Array(); + + int cols = 80; + int rows = 24; + + // Whether blinking characters are visible. Not whether blinking is enabled + // which is determined by `blink_enabled`. + bool blink_on = true; + + Vector2 cell_size = Vector2(0, 0); + + std::map palette = {}; + + tsm_age_t framebuffer_age; + + Ref last_input_event_key; + + bool selecting = false; + SelectionMode selection_mode = SelectionMode::NONE; + + static void _bell_cb(tsm_vte *vte, void *data); + static int _text_draw_cb(tsm_screen *con, uint64_t id, const uint32_t *ch, + size_t len, unsigned int width, unsigned int col, + unsigned int row, const tsm_screen_attr *attr, + tsm_age_t age, void *data); + static void _write_cb(tsm_vte *vte, const char *u8, size_t len, void *data); + + void _draw_background(int row, int col, Color bgcol, int width); + void _draw_foreground(int row, int col, char *ch, const tsm_screen_attr *attr, + Color fgcol); + ColorPair _get_cell_colors(const tsm_screen_attr *attr); + void _handle_key_input(Ref event); + void _handle_mouse_wheel(Ref event); + void _handle_selection(Ref event); + void _recalculate_size(); + void _refresh(); + void _update_theme_item_cache(); +}; + +VARIANT_ENUM_CAST(Terminal::UpdateMode); + +#endif // GODOT_XTERM_TERMINAL_H diff --git a/addons/godot_xterm/shaders/background.gdshader b/addons/godot_xterm/shaders/background.gdshader new file mode 100644 index 0000000..97df560 --- /dev/null +++ b/addons/godot_xterm/shaders/background.gdshader @@ -0,0 +1,38 @@ +shader_type canvas_item; + +uniform int cols; +uniform int rows; +uniform vec2 size; // Total size of the Terminal control. +uniform vec2 cell_size; +uniform vec4 background; +uniform sampler2D cell_colors; // Texture containing the cell colors, one pixel per cell. + +const int INVERSE_FLAG = 1 << 0; +// Texture containing the cell attributes encoded using bit flags, one flag per cell in the R channel. +uniform sampler2D attributes; + +void fragment() { + // Grid means the part of the terminal that contains the cells. + // Will vary depending on font size and the size of the control. + vec2 grid_size = vec2(cell_size.x * float(cols), cell_size.y * float(rows)); + + if (UV.x * size.x < grid_size.x && UV.y * size.y < grid_size.y) { + vec2 grid_uv = UV * size / cell_size; + + int cell_x = int(grid_uv.x); + int cell_y = int(grid_uv.y); + + vec2 sample_uv = (vec2(float(cell_x), float(cell_y)) + 0.5) / vec2(float(cols), float(rows)); + vec4 color = texture(cell_colors, sample_uv); + + int attr_flags = int(texture(attributes, sample_uv).r * 255.0 + 0.5); + if ((attr_flags & INVERSE_FLAG) != 0) { + color = vec4(1.0 - color.rgb, color.a); + } + + COLOR = color; + } else { + // If outside the grid area, use the background color. + COLOR = background; + } +} diff --git a/addons/godot_xterm/shaders/foreground.gdshader b/addons/godot_xterm/shaders/foreground.gdshader new file mode 100644 index 0000000..7812914 --- /dev/null +++ b/addons/godot_xterm/shaders/foreground.gdshader @@ -0,0 +1,7 @@ +shader_type canvas_item; + +void fragment() { + COLOR = texture(TEXTURE, UV); + // TODO: Check blink attribute and hide/show using the below. + //COLOR = vec4(0); +}