From 5b6cc99cd81c584fb4c608feef57b3cd1f25eead Mon Sep 17 00:00:00 2001 From: gumaciel <20030153+gumaciel@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:31:33 +0000 Subject: [PATCH 1/2] Add GDExtension boilerplate and basic Shell C++ node --- .gitignore | 11 ++++++++++ .gitmodules | 3 +++ SConstruct | 42 ++++++++++++++++++++++++++++++++++++ demo/shell.gdextension | 21 ++++++++++++++++++ godot-cpp | 1 + src/register_types.cpp | 33 +++++++++++++++++++++++++++++ src/register_types.h | 11 ++++++++++ src/shell.cpp | 48 ++++++++++++++++++++++++++++++++++++++++++ src/shell.h | 27 ++++++++++++++++++++++++ 9 files changed, 197 insertions(+) create mode 100644 .gitmodules create mode 100644 SConstruct create mode 100644 demo/shell.gdextension create mode 160000 godot-cpp create mode 100644 src/register_types.cpp create mode 100644 src/register_types.h create mode 100644 src/shell.cpp create mode 100644 src/shell.h diff --git a/.gitignore b/.gitignore index 50335d9..c155584 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,14 @@ export_credentials.cfg .mono/ data_*/ mono_crash.*.json + +# C++ build artifacts +*.os +*.o +*.so +*.dll +*.dylib +*.framework +.sconsign.dblite +bin/ +demo/bin/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..29bd727 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "godot-cpp"] + path = godot-cpp + url = https://github.com/godotengine/godot-cpp.git diff --git a/SConstruct b/SConstruct new file mode 100644 index 0000000..cb658fc --- /dev/null +++ b/SConstruct @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +env = SConscript("godot-cpp/SConstruct") + +# For the reference: +# - CCFLAGS are compilation flags shared between C and C++ +# - CFLAGS are for C-specific compilation flags +# - CXXFLAGS are for C++-specific compilation flags +# - CPPFLAGS are for pre-processor flags +# - CPPDEFINES are for pre-processor defines +# - LINKFLAGS are for linking flags + +# tweak this if you want to use different folders, or more folders, to store your source code in. +env.Append(CPPPATH=["src/"]) +sources = Glob("src/*.cpp") + +if env["platform"] == "macos": + library = env.SharedLibrary( + "demo/bin/libshell.{}.{}.framework/libshell.{}.{}".format( + env["platform"], env["target"], env["platform"], env["target"] + ), + source=sources, + ) +elif env["platform"] == "ios": + if env["ios_simulator"]: + library = env.StaticLibrary( + "demo/bin/libshell.{}.{}.simulator.a".format(env["platform"], env["target"]), + source=sources, + ) + else: + library = env.StaticLibrary( + "demo/bin/libshell.{}.{}.a".format(env["platform"], env["target"]), + source=sources, + ) +else: + library = env.SharedLibrary( + "demo/bin/libshell{}{}".format(env["suffix"], env["SHLIBSUFFIX"]), + source=sources, + ) + +env.NoCache(library) +Default(library) diff --git a/demo/shell.gdextension b/demo/shell.gdextension new file mode 100644 index 0000000..fb901e2 --- /dev/null +++ b/demo/shell.gdextension @@ -0,0 +1,21 @@ +[gd_resource type="GDExtension" format=3] + +[configuration] + +entry_symbol = "shell_library_init" +compatibility_minimum = "4.2" + +[libraries] + +linux.debug.x86_64 = "res://bin/libshell.linux.template_debug.x86_64.so" +linux.release.x86_64 = "res://bin/libshell.linux.template_release.x86_64.so" +linux.editor.x86_64 = "res://bin/libshell.linux.editor.x86_64.so" +windows.debug.x86_64 = "res://bin/libshell.windows.template_debug.x86_64.dll" +windows.release.x86_64 = "res://bin/libshell.windows.template_release.x86_64.dll" +windows.editor.x86_64 = "res://bin/libshell.windows.editor.x86_64.dll" +macos.debug = "res://bin/libshell.macos.template_debug.framework" +macos.release = "res://bin/libshell.macos.template_release.framework" +macos.editor = "res://bin/libshell.macos.editor.framework" +macos.debug.arm64 = "res://bin/libshell.macos.template_debug.arm64.framework" +macos.release.arm64 = "res://bin/libshell.macos.template_release.arm64.framework" +macos.editor.arm64 = "res://bin/libshell.macos.editor.arm64.framework" diff --git a/godot-cpp b/godot-cpp new file mode 160000 index 0000000..9ae37ac --- /dev/null +++ b/godot-cpp @@ -0,0 +1 @@ +Subproject commit 9ae37ac8b9b14df5284dc3d4bf87e7d8b3327503 diff --git a/src/register_types.cpp b/src/register_types.cpp new file mode 100644 index 0000000..dbfb3bc --- /dev/null +++ b/src/register_types.cpp @@ -0,0 +1,33 @@ +#include "register_types.h" +#include "shell.h" +#include +#include +#include + +using namespace godot; + +void initialize_shell_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + + ClassDB::register_class(); +} + +void uninitialize_shell_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } +} + +extern "C" { +GDExtensionBool GDE_EXPORT shell_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { + godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); + + init_obj.register_initializer(initialize_shell_module); + init_obj.register_terminator(uninitialize_shell_module); + init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); + + return init_obj.init(); +} +} diff --git a/src/register_types.h b/src/register_types.h new file mode 100644 index 0000000..c3686da --- /dev/null +++ b/src/register_types.h @@ -0,0 +1,11 @@ +#ifndef SHELL_REGISTER_TYPES_H +#define SHELL_REGISTER_TYPES_H + +#include + +using namespace godot; + +void initialize_shell_module(ModuleInitializationLevel p_level); +void uninitialize_shell_module(ModuleInitializationLevel p_level); + +#endif // SHELL_REGISTER_TYPES_H diff --git a/src/shell.cpp b/src/shell.cpp new file mode 100644 index 0000000..168530c --- /dev/null +++ b/src/shell.cpp @@ -0,0 +1,48 @@ +#include +#include "shell.h" +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define POPEN _popen +#define PCLOSE _pclose +#else +#define POPEN popen +#define PCLOSE pclose +#endif + +using namespace godot; + +void Shell::_bind_methods() { + ClassDB::bind_method(D_METHOD("execute_command", "command"), &Shell::execute_command); +} + +Shell::Shell() { +} + +Shell::~Shell() { +} + +void Shell::_process(double delta) { +} + +String Shell::execute_command(const String &p_command) { + std::string cmd = p_command.utf8().get_data(); + std::array buffer; + std::string result; + std::unique_ptr pipe(POPEN(cmd.c_str(), "r"), PCLOSE); + + if (!pipe) { + UtilityFunctions::printerr("popen() failed!"); + return String("Error executing command."); + } + + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { + result += buffer.data(); + } + + return String(result.c_str()); +} diff --git a/src/shell.h b/src/shell.h new file mode 100644 index 0000000..7219969 --- /dev/null +++ b/src/shell.h @@ -0,0 +1,27 @@ +#ifndef SHELL_H +#define SHELL_H + +#include +#include +#include + +namespace godot { + +class Shell : public Node { + GDCLASS(Shell, Node) + +protected: + static void _bind_methods(); + +public: + Shell(); + ~Shell(); + + void _process(double delta) override; + + String execute_command(const String &p_command); +}; + +} // namespace godot + +#endif // SHELL_H From cbf7d30457b8075c172ad3cbeeb3a6c7fd969f6f Mon Sep 17 00:00:00 2001 From: gumaciel <20030153+gumaciel@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:37:27 +0000 Subject: [PATCH 2/2] Refactor GDExtension Shell to use a proper PTY and libvterm ANSI emulator --- .gitmodules | 3 + SConstruct | 18 ++- libvterm | 1 + src/register_types.cpp | 33 ----- src/register_types.h | 11 -- src/shell.cpp | 48 ------- src/shell.h | 27 ---- src/shell_pty.cpp | 152 +++++++++++++++++++++++ src/shell_pty.h | 36 ++++++ src/terminal.cpp | 275 +++++++++++++++++++++++++++++++++++++++++ src/terminal.h | 61 +++++++++ 11 files changed, 536 insertions(+), 129 deletions(-) create mode 160000 libvterm delete mode 100644 src/register_types.cpp delete mode 100644 src/register_types.h delete mode 100644 src/shell.cpp delete mode 100644 src/shell.h create mode 100644 src/shell_pty.cpp create mode 100644 src/shell_pty.h create mode 100644 src/terminal.cpp create mode 100644 src/terminal.h diff --git a/.gitmodules b/.gitmodules index 29bd727..b482555 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "godot-cpp"] path = godot-cpp url = https://github.com/godotengine/godot-cpp.git +[submodule "libvterm"] + path = libvterm + url = https://github.com/neovim/libvterm.git diff --git a/SConstruct b/SConstruct index cb658fc..54ae220 100644 --- a/SConstruct +++ b/SConstruct @@ -2,18 +2,16 @@ env = SConscript("godot-cpp/SConstruct") -# For the reference: -# - CCFLAGS are compilation flags shared between C and C++ -# - CFLAGS are for C-specific compilation flags -# - CXXFLAGS are for C++-specific compilation flags -# - CPPFLAGS are for pre-processor flags -# - CPPDEFINES are for pre-processor defines -# - LINKFLAGS are for linking flags - -# tweak this if you want to use different folders, or more folders, to store your source code in. -env.Append(CPPPATH=["src/"]) +env.Append(CPPPATH=["src/", "libvterm/include"]) sources = Glob("src/*.cpp") +libvterm_sources = Glob("libvterm/src/*.c") +sources += libvterm_sources + +if env["platform"] in ["linux", "linuxbsd"]: + env.Append(LIBS=["util"]) + env.Append(CPPDEFINES=["_GNU_SOURCE"]) + if env["platform"] == "macos": library = env.SharedLibrary( "demo/bin/libshell.{}.{}.framework/libshell.{}.{}".format( diff --git a/libvterm b/libvterm new file mode 160000 index 0000000..934bc2f --- /dev/null +++ b/libvterm @@ -0,0 +1 @@ +Subproject commit 934bc2fbf21800ac3458a499df8820ca5fb45fd3 diff --git a/src/register_types.cpp b/src/register_types.cpp deleted file mode 100644 index dbfb3bc..0000000 --- a/src/register_types.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "register_types.h" -#include "shell.h" -#include -#include -#include - -using namespace godot; - -void initialize_shell_module(ModuleInitializationLevel p_level) { - if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { - return; - } - - ClassDB::register_class(); -} - -void uninitialize_shell_module(ModuleInitializationLevel p_level) { - if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { - return; - } -} - -extern "C" { -GDExtensionBool GDE_EXPORT shell_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { - godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); - - init_obj.register_initializer(initialize_shell_module); - init_obj.register_terminator(uninitialize_shell_module); - init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); - - return init_obj.init(); -} -} diff --git a/src/register_types.h b/src/register_types.h deleted file mode 100644 index c3686da..0000000 --- a/src/register_types.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef SHELL_REGISTER_TYPES_H -#define SHELL_REGISTER_TYPES_H - -#include - -using namespace godot; - -void initialize_shell_module(ModuleInitializationLevel p_level); -void uninitialize_shell_module(ModuleInitializationLevel p_level); - -#endif // SHELL_REGISTER_TYPES_H diff --git a/src/shell.cpp b/src/shell.cpp deleted file mode 100644 index 168530c..0000000 --- a/src/shell.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include -#include "shell.h" -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#define POPEN _popen -#define PCLOSE _pclose -#else -#define POPEN popen -#define PCLOSE pclose -#endif - -using namespace godot; - -void Shell::_bind_methods() { - ClassDB::bind_method(D_METHOD("execute_command", "command"), &Shell::execute_command); -} - -Shell::Shell() { -} - -Shell::~Shell() { -} - -void Shell::_process(double delta) { -} - -String Shell::execute_command(const String &p_command) { - std::string cmd = p_command.utf8().get_data(); - std::array buffer; - std::string result; - std::unique_ptr pipe(POPEN(cmd.c_str(), "r"), PCLOSE); - - if (!pipe) { - UtilityFunctions::printerr("popen() failed!"); - return String("Error executing command."); - } - - while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { - result += buffer.data(); - } - - return String(result.c_str()); -} diff --git a/src/shell.h b/src/shell.h deleted file mode 100644 index 7219969..0000000 --- a/src/shell.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef SHELL_H -#define SHELL_H - -#include -#include -#include - -namespace godot { - -class Shell : public Node { - GDCLASS(Shell, Node) - -protected: - static void _bind_methods(); - -public: - Shell(); - ~Shell(); - - void _process(double delta) override; - - String execute_command(const String &p_command); -}; - -} // namespace godot - -#endif // SHELL_H diff --git a/src/shell_pty.cpp b/src/shell_pty.cpp new file mode 100644 index 0000000..5c074df --- /dev/null +++ b/src/shell_pty.cpp @@ -0,0 +1,152 @@ +#include "shell_pty.h" + +#include +#include + +#if defined(_WIN32) +// Windows ConPTY placeholder (requires Windows 10 SDK) +#else +#include +#include +#if defined(__APPLE__) || defined(__FreeBSD__) +#include +#else +#include +#endif +#include +#include +#include +#include +#endif + +using namespace godot; + +void PTY::_bind_methods() { + ClassDB::bind_method(D_METHOD("start", "command", "rows", "cols"), &PTY::start, DEFVAL(24), DEFVAL(80)); + ClassDB::bind_method(D_METHOD("stop"), &PTY::stop); + ClassDB::bind_method(D_METHOD("read_data"), &PTY::read_data); + ClassDB::bind_method(D_METHOD("write_data", "data"), &PTY::write_data); + ClassDB::bind_method(D_METHOD("resize", "rows", "cols"), &PTY::resize); + ClassDB::bind_method(D_METHOD("is_running"), &PTY::is_running); +} + +PTY::PTY() { + master_fd = -1; + pid = -1; +} + +PTY::~PTY() { + stop(); +} + +bool PTY::start(const String &p_command, int p_rows, int p_cols) { + if (is_running()) { + stop(); + } + +#if defined(_WIN32) + UtilityFunctions::printerr("Windows ConPTY not yet implemented in this proof-of-concept."); + return false; +#else + struct winsize winp; + winp.ws_row = p_rows; + winp.ws_col = p_cols; + winp.ws_xpixel = 0; + winp.ws_ypixel = 0; + + pid = forkpty(&master_fd, nullptr, nullptr, &winp); + + if (pid < 0) { + UtilityFunctions::printerr("forkpty failed"); + return false; + } else if (pid == 0) { + // Child process + setenv("TERM", "xterm-256color", 1); + + std::string cmd = p_command.utf8().get_data(); + if (cmd.empty()) { + cmd = "/bin/sh"; + } + + const char *args[] = { cmd.c_str(), nullptr }; + execvp(args[0], (char *const *)args); + + // If execvp fails + exit(1); + } + + // Parent process + // Set master_fd to non-blocking + int flags = fcntl(master_fd, F_GETFL, 0); + fcntl(master_fd, F_SETFL, flags | O_NONBLOCK); + + return true; +#endif +} + +void PTY::stop() { +#if !defined(_WIN32) + if (pid > 0) { + kill(pid, SIGTERM); + waitpid(pid, nullptr, 0); + pid = -1; + } + if (master_fd != -1) { + close(master_fd); + master_fd = -1; + } +#endif +} + +PackedByteArray PTY::read_data() { + PackedByteArray pba; +#if !defined(_WIN32) + if (master_fd != -1) { + char buffer[4096]; + ssize_t bytes_read = read(master_fd, buffer, sizeof(buffer)); + if (bytes_read > 0) { + pba.resize(bytes_read); + memcpy(pba.ptrw(), buffer, bytes_read); + } + } +#endif + return pba; +} + +void PTY::write_data(const PackedByteArray &p_data) { +#if !defined(_WIN32) + if (master_fd != -1 && p_data.size() > 0) { + // We ignore write errors for this simple example + [[maybe_unused]] ssize_t result = write(master_fd, p_data.ptr(), p_data.size()); + } +#endif +} + +void PTY::resize(int p_rows, int p_cols) { +#if !defined(_WIN32) + if (master_fd != -1) { + struct winsize winp; + winp.ws_row = p_rows; + winp.ws_col = p_cols; + winp.ws_xpixel = 0; + winp.ws_ypixel = 0; + ioctl(master_fd, TIOCSWINSZ, &winp); + } +#endif +} + +bool PTY::is_running() const { +#if !defined(_WIN32) + if (pid > 0) { + int status; + pid_t result = waitpid(pid, &status, WNOHANG); + if (result == 0) { + return true; + } else if (result == pid) { + // Process died, waitpid consumed the zombie + return false; + } + } +#endif + return false; +} diff --git a/src/shell_pty.h b/src/shell_pty.h new file mode 100644 index 0000000..2b10e43 --- /dev/null +++ b/src/shell_pty.h @@ -0,0 +1,36 @@ +#ifndef SHELL_PTY_H +#define SHELL_PTY_H + +#include +#include +#include + +namespace godot { + +class PTY : public RefCounted { + GDCLASS(PTY, RefCounted) + +private: + int master_fd; + int pid; + +protected: + static void _bind_methods(); + +public: + PTY(); + ~PTY(); + + bool start(const String &p_command, int p_rows, int p_cols); + void stop(); + + PackedByteArray read_data(); + void write_data(const PackedByteArray &p_data); + void resize(int p_rows, int p_cols); + + bool is_running() const; +}; + +} // namespace godot + +#endif // SHELL_PTY_H diff --git a/src/terminal.cpp b/src/terminal.cpp new file mode 100644 index 0000000..6c00a37 --- /dev/null +++ b/src/terminal.cpp @@ -0,0 +1,275 @@ +#include "terminal.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace godot; + +void Terminal::_bind_methods() { + ClassDB::bind_method(D_METHOD("start", "command"), &Terminal::start, DEFVAL("")); + ClassDB::bind_method(D_METHOD("stop"), &Terminal::stop); +} + +Terminal::Terminal() { + pty.instantiate(); + rows = 24; + cols = 80; + vterm = vterm_new(rows, cols); + vterm_set_utf8(vterm, 1); + + vterm_screen = vterm_obtain_screen(vterm); + vterm_screen_enable_altscreen(vterm_screen, 1); + + VTermScreenCallbacks cb; + memset(&cb, 0, sizeof(cb)); + cb.damage = _vterm_screen_damage; + cb.moverect = _vterm_screen_moverect; + cb.movecursor = _vterm_screen_movecursor; + cb.settermprop = _vterm_screen_settermprop; + cb.bell = _vterm_screen_bell; + cb.resize = _vterm_screen_resize; + cb.sb_pushline = _vterm_screen_sb_pushline; + cb.sb_popline = _vterm_screen_sb_popline; + vterm_screen_set_callbacks(vterm_screen, &cb, this); + + vterm_output_set_callback(vterm, _vterm_output, this); + + VTermState *state = vterm_obtain_state(vterm); + vterm_state_reset(state, 1); + vterm_screen_reset(vterm_screen, 1); + + font_size = 14; + set_process(true); +} + +Terminal::~Terminal() { + if (vterm) { + vterm_free(vterm); + vterm = nullptr; + } +} + +void Terminal::_ready() { + if (font.is_null()) { + font = ThemeDB::get_singleton()->get_fallback_font(); + font_size = ThemeDB::get_singleton()->get_fallback_font_size(); + } + set_focus_mode(FOCUS_ALL); +} + +void Terminal::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_DRAW: { + _draw(); + break; + } + case NOTIFICATION_RESIZED: { + update_term_size(); + break; + } + } +} + +void Terminal::update_term_size() { + if (font.is_null()) return; + + Vector2 size = get_size(); + + float char_width = font->get_char_size('W', font_size).x; + if (char_width <= 0.0f) char_width = 8.0f; + + float char_height = font->get_height(font_size); + if (char_height <= 0.0f) char_height = 14.0f; + + int new_cols = Math::max(1, (int)(size.x / char_width)); + int new_rows = Math::max(1, (int)(size.y / char_height)); + + if (new_cols != cols || new_rows != rows) { + cols = new_cols; + rows = new_rows; + vterm_set_size(vterm, rows, cols); + if (pty->is_running()) { + pty->resize(rows, cols); + } + queue_redraw(); + } +} + +void Terminal::_process(double delta) { + if (pty->is_running()) { + PackedByteArray data = pty->read_data(); + if (data.size() > 0) { + vterm_input_write(vterm, (const char*)data.ptr(), data.size()); + vterm_screen_flush_damage(vterm_screen); + queue_redraw(); + } + } +} + +Color Terminal::vterm_color_to_godot(const VTermColor *vcolor, bool is_fg) { + if (!vcolor) { + return is_fg ? Color(0.8, 0.8, 0.8) : Color(0, 0, 0, 1.0); + } + VTermColor c = *vcolor; + vterm_screen_convert_color_to_rgb(vterm_screen, &c); + return Color(c.rgb.red / 255.0f, c.rgb.green / 255.0f, c.rgb.blue / 255.0f); +} + +void Terminal::_draw() { + if (font.is_null() || !vterm_screen) return; + + float char_width = font->get_char_size('W', font_size).x; + if (char_width <= 0) char_width = 8; + float char_height = font->get_height(font_size); + float ascent = font->get_ascent(font_size); + + // Clear background + draw_rect(Rect2(0, 0, get_size().x, get_size().y), Color(0, 0, 0, 1.0)); + + for (int row = 0; row < rows; ++row) { + for (int col = 0; col < cols; ++col) { + VTermPos pos; + pos.row = row; + pos.col = col; + + VTermScreenCell cell; + vterm_screen_get_cell(vterm_screen, pos, &cell); + + // Draw cell background + Color bg = vterm_color_to_godot(&cell.bg, false); + if (cell.attrs.reverse) { + bg = vterm_color_to_godot(&cell.fg, true); + } + + if (bg != Color(0, 0, 0, 1)) { + draw_rect(Rect2(col * char_width, row * char_height, char_width, char_height), bg); + } + + if (cell.chars[0] == 0) continue; + + // Draw cell text + Color fg = vterm_color_to_godot(&cell.fg, true); + if (cell.attrs.reverse) { + fg = vterm_color_to_godot(&cell.bg, false); + } + + String txt; + for (int i = 0; i < VTERM_MAX_CHARS_PER_CELL && cell.chars[i] != 0; ++i) { + txt += String::chr(cell.chars[i]); + } + draw_string(font, Vector2(col * char_width, row * char_height + ascent), txt, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, fg); + } + } + + // Draw Cursor + if (has_focus()) { + VTermState *state = vterm_obtain_state(vterm); + VTermPos cpos; + vterm_state_get_cursorpos(state, &cpos); + if (cpos.row >= 0 && cpos.row < rows && cpos.col >= 0 && cpos.col < cols) { + draw_rect(Rect2(cpos.col * char_width, cpos.row * char_height, char_width, char_height), Color(1, 1, 1, 0.5)); + } + } +} + +void Terminal::_gui_input(const Ref &event) { + Ref key = event; + if (key.is_valid() && key->is_pressed()) { // Removed !key->is_echo() so holding works! + if (pty->is_running()) { + VTermModifier mod = VTERM_MOD_NONE; + if (key->is_shift_pressed()) mod = (VTermModifier)(mod | VTERM_MOD_SHIFT); + if (key->is_ctrl_pressed()) mod = (VTermModifier)(mod | VTERM_MOD_CTRL); + if (key->is_alt_pressed()) mod = (VTermModifier)(mod | VTERM_MOD_ALT); + + Key k = key->get_keycode(); + + // Map some special keys that don't have unicode characters or require VTerm handling + VTermKey vk = VTERM_KEY_NONE; + switch (k) { + case KEY_ENTER: vk = VTERM_KEY_ENTER; break; + case KEY_TAB: vk = VTERM_KEY_TAB; break; + case KEY_BACKSPACE: vk = VTERM_KEY_BACKSPACE; break; + case KEY_ESCAPE: vk = VTERM_KEY_ESCAPE; break; + case KEY_UP: vk = VTERM_KEY_UP; break; + case KEY_DOWN: vk = VTERM_KEY_DOWN; break; + case KEY_LEFT: vk = VTERM_KEY_LEFT; break; + case KEY_RIGHT: vk = VTERM_KEY_RIGHT; break; + case KEY_PAGEUP: vk = VTERM_KEY_PAGEUP; break; + case KEY_PAGEDOWN: vk = VTERM_KEY_PAGEDOWN; break; + case KEY_HOME: vk = VTERM_KEY_HOME; break; + case KEY_END: vk = VTERM_KEY_END; break; + case KEY_INSERT: vk = VTERM_KEY_INS; break; + case KEY_DELETE: vk = VTERM_KEY_DEL; break; + default: break; + } + + if (vk != VTERM_KEY_NONE) { + vterm_keyboard_key(vterm, vk, mod); + } else { + uint32_t uc = key->get_unicode(); + if (uc > 0) { + vterm_keyboard_unichar(vterm, uc, mod); + } + } + } + accept_event(); + } +} + +void Terminal::start(const String &p_command) { + if (pty->start(p_command, rows, cols)) { + queue_redraw(); + } +} + +void Terminal::stop() { + pty->stop(); +} + +void Terminal::_vterm_output(const char *s, size_t len, void *user) { + Terminal *term = static_cast(user); + if (term->pty->is_running()) { + PackedByteArray pba; + pba.resize(len); + memcpy(pba.ptrw(), s, len); + term->pty->write_data(pba); + } +} + +int Terminal::_vterm_screen_damage(VTermRect rect, void *user) { + return 1; +} + +int Terminal::_vterm_screen_moverect(VTermRect dest, VTermRect src, void *user) { + return 1; +} + +int Terminal::_vterm_screen_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) { + return 1; +} + +int Terminal::_vterm_screen_settermprop(VTermProp prop, VTermValue *val, void *user) { + return 1; +} + +int Terminal::_vterm_screen_bell(void *user) { + return 1; +} + +int Terminal::_vterm_screen_resize(int rows, int cols, void *user) { + return 1; +} + +int Terminal::_vterm_screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user) { + return 1; +} + +int Terminal::_vterm_screen_sb_popline(int cols, VTermScreenCell *cells, void *user) { + return 1; +} diff --git a/src/terminal.h b/src/terminal.h new file mode 100644 index 0000000..a39ab23 --- /dev/null +++ b/src/terminal.h @@ -0,0 +1,61 @@ +#ifndef TERMINAL_H +#define TERMINAL_H + +#include +#include +#include +#include +#include +#include +#include "shell_pty.h" +#include + +namespace godot { + +class Terminal : public Control { + GDCLASS(Terminal, Control) + +private: + Ref pty; + VTerm *vterm; + VTermScreen *vterm_screen; + + int rows; + int cols; + + Ref font; + int font_size; + + static void _vterm_output(const char *s, size_t len, void *user); + static int _vterm_screen_damage(VTermRect rect, void *user); + static int _vterm_screen_moverect(VTermRect dest, VTermRect src, void *user); + static int _vterm_screen_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user); + static int _vterm_screen_settermprop(VTermProp prop, VTermValue *val, void *user); + static int _vterm_screen_bell(void *user); + static int _vterm_screen_resize(int rows, int cols, void *user); + static int _vterm_screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user); + static int _vterm_screen_sb_popline(int cols, VTermScreenCell *cells, void *user); + + void update_term_size(); + Color vterm_color_to_godot(const VTermColor *vcolor, bool is_fg); + +protected: + static void _bind_methods(); + +public: + Terminal(); + ~Terminal(); + + void _ready() override; + void _process(double delta) override; + void _draw() override; + void _gui_input(const Ref &event) override; + void _notification(int p_what); + + void start(const String &p_command); + void stop(); +}; + +} // namespace godot + +#endif // TERMINAL_H