diff --git a/addons/godot_xterm/editor_plugins/terminal/terminal_panel.tscn b/addons/godot_xterm/editor_plugins/terminal/terminal_panel.tscn index e8ed8f4..5563776 100644 --- a/addons/godot_xterm/editor_plugins/terminal/terminal_panel.tscn +++ b/addons/godot_xterm/editor_plugins/terminal/terminal_panel.tscn @@ -2,7 +2,7 @@ [ext_resource type="Script" path="res://addons/godot_xterm/editor_plugins/terminal/terminal_panel.gd" id="1"] -[sub_resource type="Image" id="Image_84jui"] +[sub_resource type="Image" id="Image_law8x"] data = { "data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", @@ -12,7 +12,7 @@ data = { } [sub_resource type="ImageTexture" id="ImageTexture_q1uu0"] -image = SubResource("Image_84jui") +image = SubResource("Image_law8x") [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_osmrc"] bg_color = Color(0.113329, 0.129458, 0.156802, 1) diff --git a/addons/godot_xterm/native/SConstruct b/addons/godot_xterm/native/SConstruct index 150e7ea..c635fca 100644 --- a/addons/godot_xterm/native/SConstruct +++ b/addons/godot_xterm/native/SConstruct @@ -26,9 +26,17 @@ sources.append([ ]) if env['platform'] == 'linux' or env['platform'] == 'macos': - env.Append(LIBS=['util', env.File('thirdparty/libuv/build/libuv_a.a')]) + env.Append(LIBS=['util', env.File('thirdparty/libuv/build/libuv_a.a')]) else: - env.Append(CPPDEFINES=['_PTY_DISABLED']) + env.Append(LIBS=['ws2_32.lib', 'Advapi32', 'User32', 'Userenv', 'iphlpapi']) + if env["target"] == "template_release": + env.Append(LIBS=[env.File('thirdparty/libuv/build/Release/uv_a.lib')]) + else: + env.Append(LIBS=[env.File('thirdparty/libuv/build/Debug/uv_a.lib')]) + +# TODO(ast) this is a bandaid fix (see https://stackoverflow.com/questions/3007312/resolving-lnk4098-defaultlib-msvcrt-conflicts-with) +# TODO(ast) a release build needs to use msvcrt instead of msvcrtd +env.Append(LINKFLAGS=['/VERBOSE:LIB', '/NODEFAULTLIB:libcmtd.lib', '/NODEFAULTLIB:libcmt.lib', '/NODEFAULTLIB:msvcrt.lib']) if env["platform"] == "macos": library = env.SharedLibrary( diff --git a/addons/godot_xterm/native/build.sh b/addons/godot_xterm/native/build.sh index 90d8dc7..fb1a039 100755 --- a/addons/godot_xterm/native/build.sh +++ b/addons/godot_xterm/native/build.sh @@ -1,8 +1,16 @@ -#!/bin/sh +#!/bin/bash # SPDX-FileCopyrightText: 2020-2023 Leroy Hopson # SPDX-License-Identifier: MIT +# Convenience function to keep the terminal open on failure in some terminals +function fail () { + echo "Failure!" + read -p "Press any key to continue" x + exit 1 +} + +set -x set -e # Parse args. @@ -27,8 +35,10 @@ done target=${target:-template_debug} if [ "$target" == "template_debug" ]; then debug_symbols="yes" + uv_target="debug" else debug_symbols="no" + uv_target="release" fi nproc=$(nproc || sysctl -n hw.ncpu) @@ -52,6 +62,7 @@ updateSubmodules() { fi } +# TODO libtsm causes warnings due to usage of nonstandard \e escape sequence. could be replaced with standard \033 or \x1b if this causes issues updateSubmodules LIBUV_DIR ${NATIVE_DIR}/thirdparty/libuv updateSubmodules LIBTSM_DIR ${NATIVE_DIR}/thirdparty/libtsm updateSubmodules GODOT_CPP_DIR ${NATIVE_DIR}/thirdparty/godot-cpp @@ -60,23 +71,26 @@ updateSubmodules GODOT_CPP_DIR ${NATIVE_DIR}/thirdparty/godot-cpp cd ${LIBUV_DIR} mkdir build || true cd build -args="-DCMAKE_BUILD_TYPE=$target -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE \ +args="-DCMAKE_BUILD_TYPE=$uv_target -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE \ -DCMAKE_OSX_ARCHITECTURES=$(uname -m)" if [ "$target" == "template_release" ]; then args="$args -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL" else args="$args -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL" fi -cmake .. $args +cmake .. $args || fail cd .. -cmake --build build --config $target -j$nproc +cmake --build build --config $uv_target -j$nproc || fail # Build libgodot-xterm. cd ${NATIVE_DIR} -scons target=$target arch=$(uname -m) debug_symbols=$debug_symbols +scons target=$target arch=$(uname -m) debug_symbols=$debug_symbols || fail # Use Docker to build libgodot-xterm javascript. #if [ -x "$(command -v docker-compose)" ]; then # UID_GID="0:0" TARGET=$target docker-compose build javascript # UID_GID="$(id -u):$(id -g)" TARGET=$target docker-compose run --rm javascript #fi + +echo "Done!" +read -p "Press any key to exit" x \ No newline at end of file diff --git a/addons/godot_xterm/native/src/constants.cpp b/addons/godot_xterm/native/src/constants.cpp index fb4702c..c6a554b 100644 --- a/addons/godot_xterm/native/src/constants.cpp +++ b/addons/godot_xterm/native/src/constants.cpp @@ -117,6 +117,35 @@ const Terminal::KeyMap Terminal::KEY_MAP = { {{KEY_Z, 'z'}, XKB_KEY_z}, {{KEY_Z, 'Z'}, XKB_KEY_Z}, + // When CTRL is pressed, the unicode is always zero. + // But to handle CTRL+ in TSM, we still need to know the keysym + {{KEY_A, '\0'}, XKB_KEY_a}, + {{KEY_B, '\0'}, XKB_KEY_b}, + {{KEY_C, '\0'}, XKB_KEY_c}, + {{KEY_D, '\0'}, XKB_KEY_d}, + {{KEY_E, '\0'}, XKB_KEY_e}, + {{KEY_F, '\0'}, XKB_KEY_f}, + {{KEY_G, '\0'}, XKB_KEY_g}, + {{KEY_H, '\0'}, XKB_KEY_h}, + {{KEY_I, '\0'}, XKB_KEY_i}, + {{KEY_J, '\0'}, XKB_KEY_j}, + {{KEY_K, '\0'}, XKB_KEY_k}, + {{KEY_L, '\0'}, XKB_KEY_l}, + {{KEY_M, '\0'}, XKB_KEY_m}, + {{KEY_N, '\0'}, XKB_KEY_n}, + {{KEY_O, '\0'}, XKB_KEY_o}, + {{KEY_P, '\0'}, XKB_KEY_p}, + {{KEY_Q, '\0'}, XKB_KEY_q}, + {{KEY_R, '\0'}, XKB_KEY_r}, + {{KEY_S, '\0'}, XKB_KEY_s}, + {{KEY_T, '\0'}, XKB_KEY_t}, + {{KEY_U, '\0'}, XKB_KEY_u}, + {{KEY_V, '\0'}, XKB_KEY_v}, + {{KEY_W, '\0'}, XKB_KEY_w}, + {{KEY_X, '\0'}, XKB_KEY_x}, + {{KEY_Y, '\0'}, XKB_KEY_y}, + {{KEY_Z, '\0'}, XKB_KEY_z}, + {{KEY_0, '0'}, XKB_KEY_0}, {{KEY_1, '1'}, XKB_KEY_1}, {{KEY_2, '2'}, XKB_KEY_2}, diff --git a/addons/godot_xterm/native/src/pty.cpp b/addons/godot_xterm/native/src/pty.cpp index 79710e5..0493d0e 100644 --- a/addons/godot_xterm/native/src/pty.cpp +++ b/addons/godot_xterm/native/src/pty.cpp @@ -12,17 +12,20 @@ #if (defined(__linux__) || defined(__APPLE__)) && !defined(_PTY_DISABLED) #include "pty_unix.h" #include +#elif defined(_WIN32) && !defined(_PTY_DISABLED) +#include "pty_win.h" +#include #endif // Require buffer to be flushed after reaching this size. #define BUFFER_LIMIT 1048576 // 1MB -#define UV_ERR_MSG(uv_err) \ +#define UV_ERR_MSG(uv_err) \ String(uv_err_name(uv_err)) + String(": ") + String(uv_strerror(uv_err)) -#define ERR_FAIL_UV_ERR(uv_err) \ - ERR_FAIL_COND_V_MSG(uv_err < 0, PTY::uv_err_to_godot_err(uv_err), \ - UV_ERR_MSG(uv_err)) +#define ERR_FAIL_UV_ERR(uv_err) \ + ERR_FAIL_COND_V_MSG(uv_err < 0, PTY::uv_err_to_godot_err(uv_err), \ + UV_ERR_MSG(uv_err)) using namespace godot; @@ -30,60 +33,62 @@ void _alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf); void _write_cb(uv_write_t *req, int status) { std::free(req); } void _close_cb(uv_handle_t *handle) { /* no-op */ }; -void PTY::_bind_methods() { - BIND_ENUM_CONSTANT(SIGNAL_SIGHUP); - BIND_ENUM_CONSTANT(SIGNAL_SIGINT); - BIND_ENUM_CONSTANT(SIGNAL_SIGQUIT); - BIND_ENUM_CONSTANT(SIGNAL_SIGILL); - BIND_ENUM_CONSTANT(SIGNAL_SIGTRAP); - BIND_ENUM_CONSTANT(SIGNAL_SIGABRT); - BIND_ENUM_CONSTANT(SIGNAL_SIGFPE); - BIND_ENUM_CONSTANT(SIGNAL_SIGKILL); - BIND_ENUM_CONSTANT(SIGNAL_SIGSEGV); - BIND_ENUM_CONSTANT(SIGNAL_SIGPIPE); - BIND_ENUM_CONSTANT(SIGNAL_SIGALRM); - BIND_ENUM_CONSTANT(SIGNAL_SIGTERM); +void PTY::_bind_methods() +{ + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGHUP); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGINT); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGQUIT); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGILL); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGTRAP); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGABRT); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGFPE); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGKILL); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGSEGV); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGPIPE); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGALRM); + BIND_ENUM_CONSTANT(IPCSIGNAL_SIGTERM); ADD_SIGNAL(MethodInfo("data_received", PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"))); ADD_SIGNAL(MethodInfo("exited", PropertyInfo(Variant::INT, "exit_code"), PropertyInfo(Variant::INT, "signal_code"))); - ClassDB::bind_method(D_METHOD("set_cols", "num_cols"), &PTY::set_cols); - ClassDB::bind_method(D_METHOD("get_cols"), &PTY::get_cols); - ClassDB::add_property("PTY", PropertyInfo(Variant::INT, "cols"), "set_cols", "get_cols"); + ClassDB::bind_method(D_METHOD("set_cols", "num_cols"), &PTY::set_cols); + ClassDB::bind_method(D_METHOD("get_cols"), &PTY::get_cols); + ClassDB::add_property("PTY", PropertyInfo(Variant::INT, "cols"), "set_cols", "get_cols"); - ClassDB::bind_method(D_METHOD("set_rows", "num_rows"), &PTY::set_rows); - ClassDB::bind_method(D_METHOD("get_rows"), &PTY::get_rows); - ClassDB::add_property("PTY", PropertyInfo(Variant::INT, "rows"), "set_rows", "get_rows"); + ClassDB::bind_method(D_METHOD("set_rows", "num_rows"), &PTY::set_rows); + ClassDB::bind_method(D_METHOD("get_rows"), &PTY::get_rows); + ClassDB::add_property("PTY", PropertyInfo(Variant::INT, "rows"), "set_rows", "get_rows"); - ClassDB::bind_method(D_METHOD("get_env"), &PTY::get_env); - ClassDB::bind_method(D_METHOD("set_env", "env"), &PTY::set_env); - ClassDB::add_property("PTY", PropertyInfo(Variant::DICTIONARY, "env"), "set_env", "get_env"); + ClassDB::bind_method(D_METHOD("get_env"), &PTY::get_env); + ClassDB::bind_method(D_METHOD("set_env", "env"), &PTY::set_env); + ClassDB::add_property("PTY", PropertyInfo(Variant::DICTIONARY, "env"), "set_env", "get_env"); - ClassDB::bind_method(D_METHOD("get_use_os_env"), &PTY::get_use_os_env); - ClassDB::bind_method(D_METHOD("set_use_os_env", "use_os_env"), &PTY::set_use_os_env); - ClassDB::add_property("PTY", PropertyInfo(Variant::BOOL, "use_os_env"), "set_use_os_env", "get_use_os_env"); + ClassDB::bind_method(D_METHOD("get_use_os_env"), &PTY::get_use_os_env); + ClassDB::bind_method(D_METHOD("set_use_os_env", "use_os_env"), &PTY::set_use_os_env); + ClassDB::add_property("PTY", PropertyInfo(Variant::BOOL, "use_os_env"), "set_use_os_env", "get_use_os_env"); - ClassDB::bind_method(D_METHOD("set_use_threads", "enabled"), &PTY::set_use_threads); - ClassDB::bind_method(D_METHOD("is_using_threads"), &PTY::is_using_threads); - ClassDB::add_property("PTY", PropertyInfo(Variant::BOOL, "use_threads"), "set_use_threads", "is_using_threads"); + ClassDB::bind_method(D_METHOD("set_use_threads", "enabled"), &PTY::set_use_threads); + ClassDB::bind_method(D_METHOD("is_using_threads"), &PTY::is_using_threads); + ClassDB::add_property("PTY", PropertyInfo(Variant::BOOL, "use_threads"), "set_use_threads", "is_using_threads"); - ClassDB::bind_method(D_METHOD("set_terminal_path", "path"), &PTY::set_terminal_path); - ClassDB::bind_method(D_METHOD("get_terminal_path"), &PTY::get_terminal_path); - ClassDB::add_property("PTY", PropertyInfo(Variant::NODE_PATH, "terminal_path", PropertyHint::PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Terminal"), "set_terminal_path", "get_terminal_path"); + ClassDB::bind_method(D_METHOD("set_terminal_path", "path"), &PTY::set_terminal_path); + ClassDB::bind_method(D_METHOD("get_terminal_path"), &PTY::get_terminal_path); + ClassDB::add_property("PTY", PropertyInfo(Variant::NODE_PATH, "terminal_path", PropertyHint::PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Terminal"), "set_terminal_path", "get_terminal_path"); - ClassDB::bind_method(D_METHOD("get_pts_name"), &PTY::get_pts_name); + ClassDB::bind_method(D_METHOD("get_pts_name"), &PTY::get_pts_name); - ClassDB::bind_method(D_METHOD("fork", "file", "args", "cwd", "cols", "rows"), &PTY::fork, DEFVAL(""), DEFVAL(PackedStringArray()), DEFVAL("."), DEFVAL(80), DEFVAL(24)); - ClassDB::bind_method(D_METHOD("open", "cols", "rows"), &PTY::open, DEFVAL(80), DEFVAL(24)); - ClassDB::bind_method(D_METHOD("write", "data"), &PTY::write); - ClassDB::bind_method(D_METHOD("resize", "cols", "rows"), &PTY::resize); - ClassDB::bind_method(D_METHOD("resizev", "size"), &PTY::resizev); - ClassDB::bind_method(D_METHOD("kill", "signal"), &PTY::kill); + ClassDB::bind_method(D_METHOD("fork", "file", "args", "cwd", "cols", "rows"), &PTY::fork, DEFVAL(""), DEFVAL(PackedStringArray()), DEFVAL("."), DEFVAL(80), DEFVAL(24)); + ClassDB::bind_method(D_METHOD("open", "cols", "rows"), &PTY::open, DEFVAL(80), DEFVAL(24)); + ClassDB::bind_method(D_METHOD("write", "data"), &PTY::write); + ClassDB::bind_method(D_METHOD("resize", "cols", "rows"), &PTY::resize); + ClassDB::bind_method(D_METHOD("resizev", "size"), &PTY::resizev); + ClassDB::bind_method(D_METHOD("kill", "signal"), &PTY::kill); - ClassDB::bind_method(D_METHOD("_on_exit", "exit_code", "signal_code"), &PTY::_on_exit); + ClassDB::bind_method(D_METHOD("_on_exit", "exit_code", "signal_code"), &PTY::_on_exit); } -PTY::PTY() { +PTY::PTY() +{ use_threads = true; set_process_internal(false); @@ -94,76 +99,92 @@ PTY::PTY() { env["TERM"] = "xterm-256color"; env["COLORTERM"] = "truecolor"; - #if defined(__linux__) || defined(__APPLE__) uv_loop_init(&loop); uv_async_init(&loop, &async_handle, [](uv_async_t *handle) {}); uv_pipe_init(&loop, &pipe, false); +#ifdef _WIN32 + uv_pipe_init(&loop, &pipe_out, false); +#endif pipe.data = this; - #endif } -void PTY::set_cols(const int num_cols) { - if (cols != num_cols) { +void PTY::set_cols(const int num_cols) +{ + if (cols != num_cols) + { cols = num_cols; resize(cols, rows); } } -int PTY::get_cols() const { - return cols; +int PTY::get_cols() const +{ + return cols; } -void PTY::set_rows(const int num_rows) { - if (rows != num_rows) { +void PTY::set_rows(const int num_rows) +{ + if (rows != num_rows) + { rows = num_rows; resize(cols, rows); } } -int PTY::get_rows() const { - return rows; +int PTY::get_rows() const +{ + return rows; } -Dictionary PTY::get_env() const { - return env; +Dictionary PTY::get_env() const +{ + return env; } -void PTY::set_env(const Dictionary &value) { - env = value; +void PTY::set_env(const Dictionary &value) +{ + env = value; } -bool PTY::get_use_os_env() const { - return use_os_env; +bool PTY::get_use_os_env() const +{ + return use_os_env; } -void PTY::set_use_os_env(const bool value) { - use_os_env = value; +void PTY::set_use_os_env(const bool value) +{ + use_os_env = value; } -void PTY::set_use_threads(bool p_use) { +void PTY::set_use_threads(bool p_use) +{ ERR_FAIL_COND(status != STATUS_CLOSED); use_threads = p_use; } -bool PTY::is_using_threads() const { +bool PTY::is_using_threads() const +{ return use_threads; } -void PTY::set_terminal_path(NodePath p_terminal_path) { +void PTY::set_terminal_path(NodePath p_terminal_path) +{ terminal_path = p_terminal_path; Callable write = Callable(this, "write"); Callable resizev = Callable(this, "resizev"); // Disconnect the current terminal, if any. - if (terminal != nullptr) { + if (terminal != nullptr) + { disconnect("data_received", Callable(terminal, "write")); terminal->disconnect("data_sent", write); terminal->disconnect("size_changed", resizev); } terminal = Object::cast_to(get_node_or_null(terminal_path)); - if (terminal == nullptr) return; + if (terminal == nullptr) + return; // Connect the new terminal. resize(terminal->get_cols(), terminal->get_rows()); @@ -175,23 +196,31 @@ void PTY::set_terminal_path(NodePath p_terminal_path) { connect("data_received", Callable(terminal, "write"), CONNECT_PERSIST); } -NodePath PTY::get_terminal_path() const { +NodePath PTY::get_terminal_path() const +{ return terminal_path; } -String PTY::get_pts_name() const { +String PTY::get_pts_name() const +{ return pts_name; } -Error PTY::fork(const String &file, const PackedStringArray &args, const String &cwd, const int p_cols, const int p_rows) { +Error PTY::fork(const String &file, const PackedStringArray &args, const String &cwd, const int p_cols, const int p_rows) +{ String fork_file = _get_fork_file(file); Dictionary fork_env = _get_fork_env(); Dictionary result; - #if defined(__linux__) || defined(__APPLE__) +#if defined(__linux__) || defined(__APPLE__) String helper_path = ProjectSettings::get_singleton()->globalize_path("res://addons/godot_xterm/native/bin/spawn-helper"); result = PTYUnix::fork(fork_file, args, _parse_env(fork_env), cwd, p_cols, p_rows, -1, -1, true, helper_path, Callable(this, "_on_exit")); - #endif +#endif + +#if defined(_WIN32) + String helper_path = ProjectSettings::get_singleton()->globalize_path("res://addons/godot_xterm/native/bin/spawn-helper"); + result = PTYWin::fork(fork_file, args, _parse_env(fork_env), cwd, p_cols, p_rows, -1, -1, true, helper_path, Callable(this, "_on_exit")); +#endif Error err = static_cast((int)result["error"]); ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to fork."); @@ -199,15 +228,22 @@ Error PTY::fork(const String &file, const PackedStringArray &args, const String fd = result["fd"]; pid = result["pid"]; pts_name = result["pty"]; - +#ifdef _WIN32 + fd_out = result["fd_out"]; + hpc = result["hpc"]; +#endif + status = STATUS_OPEN; - #if defined(__linux__) || defined(__APPLE__) - _pipe_open(fd); - uv_read_start((uv_stream_t *)&pipe, _alloc_buffer, _read_cb); - #endif + _pipe_open(fd, &pipe); +#ifdef _WIN32 + _pipe_open(fd_out, &pipe_out); +#endif - if (use_threads) { + uv_read_start((uv_stream_t *)&pipe, _alloc_buffer, _read_cb); + + if (use_threads) + { stop_thread.clear(); thread->start(callable_mp(this, &PTY::_thread_func)); } @@ -216,80 +252,97 @@ Error PTY::fork(const String &file, const PackedStringArray &args, const String return OK; } -void PTY::kill(const int signal) { - #if (defined(__linux__) || defined(__APPLE__)) && !defined(_PTY_DISABLED) - if (pid > 0) { +void PTY::kill(const int signal) +{ +#if !defined(_PTY_DISABLED) + if (pid > 0) + { uv_kill(pid, signal); } - #endif +#endif } -Error PTY::open(const int cols, const int rows) { - Dictionary result; +Error PTY::open(const int cols, const int rows) +{ + Dictionary result; - #if defined(__linux__) || defined(__APPLE__) - result = PTYUnix::open(cols, rows); - #endif +#if defined(__linux__) || defined(__APPLE__) + result = PTYUnix::open(cols, rows); +#endif - Error err = static_cast((int)result["error"]); - ERR_FAIL_COND_V(err != OK, err); +#if defined(_WIN32) + result = PTYWin::open(cols, rows); +#endif - fd = result["master"]; - pts_name = result["pty"]; + Error err = static_cast((int)result["error"]); + ERR_FAIL_COND_V(err != OK, err); - return OK; + fd = result["master"]; + pts_name = result["pty"]; + + return OK; } -void PTY::resize(const int p_cols, const int p_rows) { +void PTY::resize(const int p_cols, const int p_rows) +{ cols = p_cols; rows = p_rows; - #if defined(__linux__) || defined(__APPLE__) - if (fd > -1) { +#if defined(__linux__) || defined(__APPLE__) + if (fd > -1) + { PTYUnix::resize(fd, cols, rows); } - #endif +#endif + +#if defined(_WIN32) + if (fd > -1) + { + PTYWin::resize(hpc, cols, rows); + } +#endif } -void PTY::write(const Variant &data) const { +void PTY::write(const Variant &data) const +{ 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."); - } + 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."); + } - if (status == STATUS_OPEN) { - #if defined(__linux__) || defined(__APPLE__) + if (status == STATUS_OPEN) + { uv_buf_t buf; buf.base = (char *)bytes.ptr(); buf.len = bytes.size(); uv_write_t *req = (uv_write_t *)malloc(sizeof(uv_write_t)); req->data = (void *)buf.base; - uv_write(req, (uv_stream_t *)&pipe, &buf, 1, _write_cb); - uv_run((uv_loop_t*)&loop, UV_RUN_NOWAIT); - #endif + uv_write(req, (uv_stream_t *)&pipe_out, &buf, 1, _write_cb); + uv_run((uv_loop_t *)&loop, UV_RUN_NOWAIT); } } -void PTY::_notification(int p_what) { +void PTY::_notification(int p_what) +{ switch (p_what) { - case NOTIFICATION_INTERNAL_PROCESS: + case NOTIFICATION_INTERNAL_PROCESS: { - #if defined(__linux__) || defined(__APPLE__) - if (!use_threads) uv_run(&loop, UV_RUN_NOWAIT); - #endif + if (!use_threads) + uv_run(&loop, UV_RUN_NOWAIT); buffer_write_mutex->lock(); - if (buffer.size() > 0) { + if (buffer.size() > 0) + { emit_signal("data_received", buffer); buffer.clear(); buffer_cleared->post(); @@ -304,46 +357,59 @@ void PTY::_notification(int p_what) { } } -void PTY::_thread_func() { - while (!stop_thread.is_set()) { - if (buffer.size() < BUFFER_LIMIT) { - #if defined(__linux__) || defined(__APPLE__) +void PTY::_thread_func() +{ + while (!stop_thread.is_set()) + { + if (buffer.size() < BUFFER_LIMIT) + { uv_run(&loop, UV_RUN_ONCE); - #endif - } else { + } + else + { buffer_cleared->wait(); } } } -void PTY::_close() { - if (use_threads) { - if (thread->is_started()) { +void PTY::_close() +{ + if (use_threads) + { + if (thread->is_started()) + { stop_thread.set(); - #if defined(__linux__) || defined(__APPLE__) uv_async_send(&async_handle); - #endif thread->wait_to_finish(); } } - #if defined(__linux__) || defined(__APPLE__) - if (!uv_is_closing((uv_handle_t *)&pipe)) { + if (!uv_is_closing((uv_handle_t *)&pipe)) + { uv_close((uv_handle_t *)&pipe, _close_cb); } - if (!uv_is_closing((uv_handle_t *)&async_handle)) { + if (!uv_is_closing((uv_handle_t *)&async_handle)) + { uv_close((uv_handle_t *)&async_handle, _close_cb); } - - if (status == STATUS_OPEN) { + + if (status == STATUS_OPEN) + { uv_run(&loop, UV_RUN_NOWAIT); uv_loop_close(&loop); } - if (fd > 0) close(fd); - if (pid > 0) kill(SIGNAL_SIGHUP); - #endif +#ifdef _WIN32 + PTYWin::close(hpc, fd, fd_out); + fd_out = -1; + hpc = -1; +#else + if (fd > 0) + close(fd); + if (pid > 0) + kill(IPCSIGNAL_SIGHUP); +#endif fd = -1; pid = -1; @@ -352,38 +418,46 @@ void PTY::_close() { status = STATUS_CLOSED; } -String PTY::_get_fork_file(const String &file) const { - if (!file.is_empty()) return file; +String PTY::_get_fork_file(const String &file) const +{ + if (!file.is_empty()) + return file; String shell_env = OS::get_singleton()->get_environment("SHELL"); - if (!shell_env.is_empty()) { + if (!shell_env.is_empty()) + { return shell_env; } - #if defined(__linux__) +#if defined(__linux__) return "sh"; - #elif defined(__APPLE__) +#elif defined(__APPLE__) return "zsh"; - #elif defined(_WIN32) +#elif defined(_WIN32) return "cmd.exe"; - #else +#else return ""; - #endif +#endif } -Dictionary PTY::_get_fork_env() const { - if (!use_os_env) return env; +Dictionary PTY::_get_fork_env() const +{ + if (!use_os_env) + return env; - #if defined(_PTY_DISABLED) +#if defined(_PTY_DISABLED) return env; - #endif +#endif + // TODO This might need windows specific adjustment + return env; Dictionary os_env; uv_env_item_t *uv_env; int count; uv_os_environ(&uv_env, &count); - for (int i = 0; i < count; i++) { + for (int i = 0; i < count; i++) + { os_env[uv_env[i].name] = uv_env[i].value; } uv_os_free_environ(uv_env, count); @@ -394,18 +468,19 @@ Dictionary PTY::_get_fork_env() const { // Make sure we didn't start our server from inside screen. // http://web.mit.edu/gnu/doc/html/screen_20.html - os_env.erase("STY"); - os_env.erase("WINDOW"); + os_env.erase("STY"); + os_env.erase("WINDOW"); // Delete some variables that might confuse our terminal. - os_env.erase("WINDOWID"); - os_env.erase("TERMCAP"); - os_env.erase("COLUMNS"); - os_env.erase("LINES"); + os_env.erase("WINDOWID"); + os_env.erase("TERMCAP"); + os_env.erase("COLUMNS"); + os_env.erase("LINES"); // Merge in our custom environment. PackedStringArray keys = PackedStringArray(env.keys()); - for (int i = 0; i < keys.size(); i++) { + for (int i = 0; i < keys.size(); i++) + { String key = keys[i]; os_env[key] = env[key]; } @@ -413,11 +488,13 @@ Dictionary PTY::_get_fork_env() const { return os_env; } -PackedStringArray PTY::_parse_env(const Dictionary &env) const { +PackedStringArray PTY::_parse_env(const Dictionary &env) const +{ PackedStringArray parsed_env; PackedStringArray keys = PackedStringArray(env.keys()); - for (int i = 0; i < keys.size(); i++) { + for (int i = 0; i < keys.size(); i++) + { String key = keys[i]; parsed_env.push_back(key + "=" + String(env[key])); } @@ -425,37 +502,41 @@ PackedStringArray PTY::_parse_env(const Dictionary &env) const { return parsed_env; } -void PTY::_on_exit(int exit_code, int exit_signal) { +void PTY::_on_exit(int exit_code, int exit_signal) +{ call_deferred("emit_signal", "exited", exit_code, exit_signal); } -#if defined(__linux__) || defined(__APPLE__) - -void _alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { - buf->base = (char *)malloc(suggested_size); - buf->len = suggested_size; +void _alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) +{ + buf->base = (char *)malloc(suggested_size); + buf->len = suggested_size; } -void PTY::_read_cb(uv_stream_t *pipe, ssize_t nread, const uv_buf_t *buf) { +void PTY::_read_cb(uv_stream_t *pipe, ssize_t nread, const uv_buf_t *buf) +{ PTY *pty = static_cast(pipe->data); - if (nread < 0) { - switch (nread) { - case UV_EOF: - // Normal after shell exits. - case UV_EIO: - // Can happen when the process exits. - // As long as PTY has caught it, we should be fine. - uv_read_stop(pipe); - pty->status = PTY::Status::STATUS_CLOSED; + if (nread < 0) + { + switch (nread) + { + case UV_EOF: + // Normal after shell exits. + case UV_EIO: + // Can happen when the process exits. + // As long as PTY has caught it, we should be fine. + uv_read_stop(pipe); + pty->status = PTY::Status::STATUS_CLOSED; + return; + default: + pty->status = PTY::Status::STATUS_ERROR; + } return; - default: - pty->status = PTY::Status::STATUS_ERROR; - } - return; } - if (nread > 0) { + if (nread > 0) + { MutexLock lock(*pty->buffer_write_mutex.ptr()); int old_size = pty->buffer.size(); @@ -468,14 +549,11 @@ void PTY::_read_cb(uv_stream_t *pipe, ssize_t nread, const uv_buf_t *buf) { } } -Error PTY::_pipe_open(const int fd) { +Error PTY::_pipe_open(const int fd, uv_pipe_t *pipe) +{ ERR_FAIL_COND_V_MSG(fd < 0, FAILED, "File descriptor must be a non-negative integer value."); - - ERR_FAIL_UV_ERR(uv_pipe_open(&pipe, fd)); - ERR_FAIL_UV_ERR(uv_stream_set_blocking((uv_stream_t *)&pipe, false)); - ERR_FAIL_UV_ERR(uv_read_start((uv_stream_t *)&pipe, _alloc_buffer, _read_cb)); + ERR_FAIL_UV_ERR(uv_pipe_open(pipe, fd)); + ERR_FAIL_UV_ERR(uv_stream_set_blocking((uv_stream_t *)pipe, false)); return OK; -} - -#endif +} \ No newline at end of file diff --git a/addons/godot_xterm/native/src/pty.h b/addons/godot_xterm/native/src/pty.h index 41e2c72..4585ac9 100644 --- a/addons/godot_xterm/native/src/pty.h +++ b/addons/godot_xterm/native/src/pty.h @@ -19,22 +19,24 @@ namespace godot GDCLASS(PTY, Node) public: - enum Signal { - SIGNAL_SIGHUP = 1, - SIGNAL_SIGINT = 2, - SIGNAL_SIGQUIT = 3, - SIGNAL_SIGILL = 4, - SIGNAL_SIGTRAP = 5, - SIGNAL_SIGABRT = 6, - SIGNAL_SIGFPE = 8, - SIGNAL_SIGKILL = 9, - SIGNAL_SIGSEGV = 11, - SIGNAL_SIGPIPE = 13, - SIGNAL_SIGALRM = 14, - SIGNAL_SIGTERM = 15, + enum IPCSignal + { + IPCSIGNAL_SIGHUP = 1, + IPCSIGNAL_SIGINT = 2, + IPCSIGNAL_SIGQUIT = 3, + IPCSIGNAL_SIGILL = 4, + IPCSIGNAL_SIGTRAP = 5, + IPCSIGNAL_SIGABRT = 6, + IPCSIGNAL_SIGFPE = 8, + IPCSIGNAL_SIGKILL = 9, + IPCSIGNAL_SIGSEGV = 11, + IPCSIGNAL_SIGPIPE = 13, + IPCSIGNAL_SIGALRM = 14, + IPCSIGNAL_SIGTERM = 15, }; - enum Status { + enum Status + { STATUS_CLOSED, STATUS_OPEN, STATUS_PAUSED, @@ -65,7 +67,7 @@ namespace godot String get_pts_name() const; Error fork(const String &file = "", const PackedStringArray &args = PackedStringArray(), const String &cwd = ".", const int cols = 80, const int rows = 24); - void kill(const int signum = Signal::SIGNAL_SIGHUP); + void kill(const int signum = IPCSignal::IPCSIGNAL_SIGHUP); Error open(const int cols = 80, const int rows = 24); void resize(const int cols, const int rows); void resizev(const Vector2i &size) { resize(size.x, size.y); }; @@ -79,6 +81,10 @@ namespace godot private: int pid = -1; int fd = -1; +#ifdef _WIN32 + int fd_out = -1; + int64_t hpc = -1; // pseudoconsole handle +#endif unsigned int cols = 80; unsigned int rows = 24; @@ -106,15 +112,17 @@ namespace godot bool use_threads; void _thread_func(); - #if defined(__linux__) || defined(__APPLE__) uv_loop_t loop; uv_pipe_t pipe; - Error _pipe_open(const int fd); - #endif +#ifdef _WIN32 + uv_pipe_t pipe_out; +#endif + + Error _pipe_open(const int fd, uv_pipe_t *pipe); static void _read_cb(uv_stream_t *pipe, ssize_t nread, const uv_buf_t *buf); static Error uv_err_to_godot_err(const int uv_err); }; } // namespace godot -VARIANT_ENUM_CAST(PTY::Signal); +VARIANT_ENUM_CAST(PTY::IPCSignal); diff --git a/addons/godot_xterm/native/src/pty_unix.cpp b/addons/godot_xterm/native/src/pty_unix.cpp index 95ea30c..4d9359b 100644 --- a/addons/godot_xterm/native/src/pty_unix.cpp +++ b/addons/godot_xterm/native/src/pty_unix.cpp @@ -55,10 +55,10 @@ /* Some platforms name VWERASE and VDISCARD differently */ #if !defined(VWERASE) && defined(VWERSE) -#define VWERASE VWERSE +#define VWERASE VWERSE #endif #if !defined(VDISCARD) && defined(VDISCRD) -#define VDISCARD VDISCRD +#define VDISCARD VDISCRD #endif /* for pty_getproc */ @@ -82,7 +82,7 @@ /* macOS 10.14 back does not define this constant */ #ifndef POSIX_SPAWN_SETSID - #define POSIX_SPAWN_SETSID 1024 +#define POSIX_SPAWN_SETSID 1024 #endif /* environ for execvpe */ @@ -91,29 +91,32 @@ extern char **environ; #endif #if defined(__APPLE__) -extern "C" { -// Changes the current thread's directory to a path or directory file -// descriptor. libpthread only exposes a syscall wrapper starting in -// macOS 10.12, but the system call dates back to macOS 10.5. On older OSes, -// the syscall is issued directly. -int pthread_chdir_np(const char* dir) API_AVAILABLE(macosx(10.12)); -int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12)); +extern "C" +{ + // Changes the current thread's directory to a path or directory file + // descriptor. libpthread only exposes a syscall wrapper starting in + // macOS 10.12, but the system call dates back to macOS 10.5. On older OSes, + // the syscall is issued directly. + int pthread_chdir_np(const char *dir) API_AVAILABLE(macosx(10.12)); + int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12)); } -#define HANDLE_EINTR(x) ({ \ - int eintr_wrapper_counter = 0; \ - decltype(x) eintr_wrapper_result; \ - do { \ - eintr_wrapper_result = (x); \ +#define HANDLE_EINTR(x) ({ \ + int eintr_wrapper_counter = 0; \ + decltype(x) eintr_wrapper_result; \ + do \ + { \ + eintr_wrapper_result = (x); \ } while (eintr_wrapper_result == -1 && errno == EINTR && \ - eintr_wrapper_counter++ < 100); \ - eintr_wrapper_result; \ + eintr_wrapper_counter++ < 100); \ + eintr_wrapper_result; \ }) #endif using namespace godot; -static void await_exit(Callable cb, pid_t pid) { +static void await_exit(Callable cb, pid_t pid) +{ int ret; int stat_loc; #if defined(__APPLE__) @@ -123,8 +126,10 @@ static void await_exit(Callable cb, pid_t pid) { struct kevent change = {0}; EV_SET(&change, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL); ret = HANDLE_EINTR(kevent(kq, &change, 1, NULL, 0, NULL)); - if (ret == -1) { - if (errno == ESRCH) { + if (ret == -1) + { + if (errno == ESRCH) + { // At this point, one of the following has occurred: // 1. The process has died but has not yet been reaped. // 2. The process has died and has already been reaped. @@ -132,19 +137,25 @@ static void await_exit(Callable cb, pid_t pid) { // kqueueable, but it may not be waitable yet either. Mark calls // this case the "zombie death race". ret = HANDLE_EINTR(waitpid(pid, &stat_loc, WNOHANG)); - if (ret == 0) { + if (ret == 0) + { ret = kill(pid, SIGKILL); - if (ret != -1) { + if (ret != -1) + { HANDLE_EINTR(waitpid(pid, &stat_loc, 0)); } } } - } else { + } + else + { struct kevent event = {0}; ret = HANDLE_EINTR(kevent(kq, NULL, 0, &event, 1, NULL)); - if (ret == 1) { + if (ret == 1) + { if ((event.fflags & NOTE_EXIT) && - (event.ident == static_cast(pid))) { + (event.ident == static_cast(pid))) + { // The process is dead or dying. This won't block for long, if at // all. HANDLE_EINTR(waitpid(pid, &stat_loc, 0)); @@ -152,16 +163,22 @@ static void await_exit(Callable cb, pid_t pid) { } } #else - while (true) { + while (true) + { errno = 0; - if ((ret = waitpid(pid, &stat_loc, 0)) != pid) { - if (ret == -1 && errno == EINTR) { + if ((ret = waitpid(pid, &stat_loc, 0)) != pid) + { + if (ret == -1 && errno == EINTR) + { continue; } - if (ret == -1 && errno == ECHILD) { + if (ret == -1 && errno == ECHILD) + { // waitpid is already handled elsewhere. ; - } else { + } + else + { assert(false); } } @@ -169,22 +186,26 @@ static void await_exit(Callable cb, pid_t pid) { } #endif int exit_code = 0, signal_code = 0; - if (WIFEXITED(stat_loc)) { + if (WIFEXITED(stat_loc)) + { exit_code = WEXITSTATUS(stat_loc); // errno? } - if (WIFSIGNALED(stat_loc)) { + if (WIFSIGNALED(stat_loc)) + { signal_code = WTERMSIG(stat_loc); } cb.call_deferred(exit_code, signal_code); } -static void on_exit(int exit_code, int signal_code, Callable cb, Thread *thread) { +static void on_exit(int exit_code, int signal_code, Callable cb, Thread *thread) +{ cb.call(exit_code, signal_code); thread->wait_to_finish(); } -void setup_exit_callback(Callable cb, pid_t pid) { +void setup_exit_callback(Callable cb, pid_t pid) +{ Thread *thread = memnew(Thread); Callable exit_func = create_custom_callable_static_function_pointer(&on_exit).bind(cb, thread); @@ -210,18 +231,20 @@ pty_getproc(int, char *); #if defined(__APPLE__) || defined(__OpenBSD__) static void -pty_posix_spawn(char** argv, char** env, +pty_posix_spawn(char **argv, char **env, const struct termios *termp, const struct winsize *winp, - int* master, - pid_t* pid, - int* err); + int *master, + pid_t *pid, + int *err); #endif -struct DelBuf { +struct DelBuf +{ int len; DelBuf(int len) : len(len) {} - void operator()(char **p) { + void operator()(char **p) + { if (p == nullptr) return; for (int i = 0; i < len; i++) @@ -231,18 +254,18 @@ struct DelBuf { }; Dictionary PTYUnix::fork( - const String &p_file, - const PackedStringArray &p_args, - const PackedStringArray &p_env, - const String &p_cwd, - const int &p_cols, - const int &p_rows, - const int &p_uid, - const int &p_gid, - const bool &p_utf8, - const String &p_helper_path, - const Callable &p_on_exit -) { + const String &p_file, + const PackedStringArray &p_args, + const PackedStringArray &p_env, + const String &p_cwd, + const int &p_cols, + const int &p_rows, + const int &p_uid, + const int &p_gid, + const bool &p_utf8, + const String &p_helper_path, + const Callable &p_on_exit) +{ Dictionary result; result["error"] = FAILED; @@ -258,7 +281,8 @@ Dictionary PTYUnix::fork( std::unique_ptr env_unique_ptr(new char *[envc + 1], DelBuf(envc + 1)); char **env = env_unique_ptr.get(); env[envc] = NULL; - for (int i = 0; i < envc; i++) { + for (int i = 0; i < envc; i++) + { std::string pair = env_[i].utf8().get_data(); env[i] = strdup(pair.c_str()); } @@ -276,14 +300,15 @@ Dictionary PTYUnix::fork( #if !defined(__APPLE__) // uid / gid int uid = p_uid; - int gid = p_gid; + int gid = p_gid; #endif // termios struct termios t = termios(); struct termios *term = &t; term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT; - if (p_utf8) { + if (p_utf8) + { #if defined(IUTF8) term->c_iflag |= IUTF8; #endif @@ -309,10 +334,10 @@ Dictionary PTYUnix::fork( term->c_cc[VMIN] = 1; term->c_cc[VTIME] = 0; - #if (__APPLE__) +#if (__APPLE__) term->c_cc[VDSUSP] = 25; term->c_cc[VSTATUS] = 20; - #endif +#endif cfsetispeed(term, B38400); cfsetospeed(term, B38400); @@ -331,27 +356,31 @@ Dictionary PTYUnix::fork( argv[1] = strdup(cwd_.c_str()); argv[2] = strdup(file.c_str()); argv[argl - 1] = NULL; - for (int i = 0; i < argc; i++) { + for (int i = 0; i < argc; i++) + { std::string arg = argv_[i].utf8().get_data(); argv[i + 3] = strdup(arg.c_str()); } int err = -1; pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err); - if (err != 0) { + if (err != 0) + { ERR_FAIL_V_MSG(result, "posix_spawnp failed with error: " + String(strerror(err))); } - if (pty_nonblock(master) == -1) { + if (pty_nonblock(master) == -1) + { ERR_FAIL_V_MSG(result, "Could not set master fd to nonblocking."); } #else int argc = argv_.size(); int argl = argc + 2; std::unique_ptr argv_unique_ptr(new char *[argl], DelBuf(argl)); - char** argv = argv_unique_ptr.get(); + char **argv = argv_unique_ptr.get(); argv[0] = strdup(file.c_str()); argv[argl - 1] = NULL; - for (int i = 0; i < argc; i++) { + for (int i = 0; i < argc; i++) + { std::string arg = argv_[i].utf8().get_data(); argv[i + 1] = strdup(arg.c_str()); } @@ -365,14 +394,16 @@ Dictionary PTYUnix::fork( sigfillset(&newmask); pthread_sigmask(SIG_SETMASK, &newmask, &oldmask); - pid = forkpty(&master, nullptr, static_cast(term), static_cast(&winp)); + pid = forkpty(&master, nullptr, static_cast(term), static_cast(&winp)); - if (!pid) { + if (!pid) + { // remove all signal handler from child sig_action.sa_handler = SIG_DFL; sig_action.sa_flags = 0; sigemptyset(&sig_action.sa_mask); - for (int i = 0 ; i < NSIG ; i++) { // NSIG is a macro for all signals + 1 + for (int i = 0; i < NSIG; i++) + { // NSIG is a macro for all signals + 1 sigaction(i, &sig_action, NULL); } } @@ -380,44 +411,51 @@ Dictionary PTYUnix::fork( // re-enable signals pthread_sigmask(SIG_SETMASK, &oldmask, NULL); - switch (pid) { - case -1: - ERR_FAIL_V_MSG(result, "forkpty(3) failed."); - case 0: - if (strlen(cwd_.c_str())) { - if (chdir(cwd_.c_str()) == -1) { - perror("chdir(2) failed."); - _exit(1); - } - } - - if (uid != -1 && gid != -1) { - if (setgid(gid) == -1) { - perror("setgid(2) failed."); - _exit(1); - } - if (setuid(uid) == -1) { - perror("setuid(2) failed."); - _exit(1); - } - } - + switch (pid) + { + case -1: + ERR_FAIL_V_MSG(result, "forkpty(3) failed."); + case 0: + if (strlen(cwd_.c_str())) + { + if (chdir(cwd_.c_str()) == -1) { - char **old = environ; - environ = env; - execvp(argv[0], argv); - environ = old; - perror("execvp(3) failed."); + perror("chdir(2) failed."); _exit(1); } - default: - if (pty_nonblock(master) == -1) { - ERR_FAIL_V_MSG(result, "Could not set master fd to nonblocking."); + } + + if (uid != -1 && gid != -1) + { + if (setgid(gid) == -1) + { + perror("setgid(2) failed."); + _exit(1); } + if (setuid(uid) == -1) + { + perror("setuid(2) failed."); + _exit(1); + } + } + + { + char **old = environ; + environ = env; + execvp(argv[0], argv); + environ = old; + perror("execvp(3) failed."); + _exit(1); + } + default: + if (pty_nonblock(master) == -1) + { + ERR_FAIL_V_MSG(result, "Could not set master fd to nonblocking."); + } } #endif - result["fd"] = master; + result["fd"] = master; result["pid"] = pid; result["pty"] = ptsname(master); @@ -430,9 +468,9 @@ Dictionary PTYUnix::fork( } Dictionary PTYUnix::open( - const int &p_cols, - const int &p_rows -) { + const int &p_cols, + const int &p_rows) +{ Dictionary result; result["error"] = FAILED; @@ -445,51 +483,56 @@ Dictionary PTYUnix::open( // pty int master, slave; - int ret = openpty(&master, &slave, nullptr, NULL, static_cast(&winp)); + int ret = openpty(&master, &slave, nullptr, NULL, static_cast(&winp)); - if (ret == -1) { + if (ret == -1) + { ERR_FAIL_V_MSG(result, "openpty(3) failed."); } - if (pty_nonblock(master) == -1) { + if (pty_nonblock(master) == -1) + { ERR_FAIL_V_MSG(result, "Could not set master fd to nonblocking."); } - if (pty_nonblock(slave) == -1) { + if (pty_nonblock(slave) == -1) + { ERR_FAIL_V_MSG(result, "Could not set slave fd to nonblocking."); } result["master"] = master; result["slave"] = slave; - result["pty"] = ptsname(master); + result["pty"] = ptsname(master); result["error"] = OK; return result; } void PTYUnix::resize( - const int &p_fd, - const int &p_cols, - const int &p_rows -) { + const int &p_fd, + const int &p_cols, + const int &p_rows) +{ int fd = p_fd; struct winsize winp; - winp.ws_col = p_cols; - winp.ws_row = p_rows; + winp.ws_col = p_cols; + winp.ws_row = p_rows; winp.ws_xpixel = 0; winp.ws_ypixel = 0; - if (ioctl(fd, TIOCSWINSZ, &winp) == -1) { - switch (errno) { - case EBADF: - ERR_FAIL_MSG("ioctl(2) failed, EBADF"); - case EFAULT: - ERR_FAIL_MSG("ioctl(2) failed, EFAULT"); - case EINVAL: - ERR_FAIL_MSG("ioctl(2) failed, EINVAL"); - case ENOTTY: - ERR_FAIL_MSG("ioctl(2) failed, ENOTTY"); + if (ioctl(fd, TIOCSWINSZ, &winp) == -1) + { + switch (errno) + { + case EBADF: + ERR_FAIL_MSG("ioctl(2) failed, EBADF"); + case EFAULT: + ERR_FAIL_MSG("ioctl(2) failed, EFAULT"); + case EINVAL: + ERR_FAIL_MSG("ioctl(2) failed, EINVAL"); + case ENOTTY: + ERR_FAIL_MSG("ioctl(2) failed, ENOTTY"); } ERR_FAIL_MSG("ioctl(2) failed"); } @@ -501,14 +544,14 @@ void PTYUnix::resize( * Foreground Process Name */ String process( - const int &p_fd, - const String &p_tty -) { + const int &p_fd, + const String &p_tty) +{ #if defined(__APPLE__) int fd = p_fd; char *name = pty_getproc(fd); #else - int fd = p_fd; + int fd = p_fd; std::string tty_ = p_tty.utf8().get_data(); char *tty = strdup(tty_.c_str()); @@ -516,7 +559,8 @@ String process( free(tty); #endif - if (name == NULL) { + if (name == NULL) + { return ""; } @@ -530,9 +574,11 @@ String process( */ static int -pty_nonblock(int fd) { +pty_nonblock(int fd) +{ int flags = fcntl(fd, F_GETFL, 0); - if (flags == -1) return -1; + if (flags == -1) + return -1; return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } @@ -561,7 +607,8 @@ pty_nonblock(int fd) { #if defined(__linux__) static char * -pty_getproc(int fd, char *tty) { +pty_getproc(int fd, char *tty) +{ FILE *f; char *path, *buf; size_t len; @@ -569,14 +616,17 @@ pty_getproc(int fd, char *tty) { pid_t pgrp; int r; - if ((pgrp = tcgetpgrp(fd)) == -1) { + if ((pgrp = tcgetpgrp(fd)) == -1) + { return NULL; } r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp); - if (r == -1 || path == NULL) return NULL; + if (r == -1 || path == NULL) + return NULL; - if ((f = fopen(path, "r")) == NULL) { + if ((f = fopen(path, "r")) == NULL) + { free(path); return NULL; } @@ -585,14 +635,18 @@ pty_getproc(int fd, char *tty) { len = 0; buf = NULL; - while ((ch = fgetc(f)) != EOF) { - if (ch == '\0') break; + while ((ch = fgetc(f)) != EOF) + { + if (ch == '\0') + break; buf = (char *)realloc(buf, len + 2); - if (buf == NULL) return NULL; + if (buf == NULL) + return NULL; buf[len++] = ch; } - if (buf != NULL) { + if (buf != NULL) + { buf[len] = '\0'; } @@ -603,21 +657,25 @@ pty_getproc(int fd, char *tty) { #elif defined(__APPLE__) static char * -pty_getproc(int fd) { - int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, 0 }; +pty_getproc(int fd) +{ + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, 0}; size_t size; struct kinfo_proc kp; - if ((mib[3] = tcgetpgrp(fd)) == -1) { + if ((mib[3] = tcgetpgrp(fd)) == -1) + { return NULL; } size = sizeof kp; - if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) { + if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) + { return NULL; } - if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') { + if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') + { return NULL; } @@ -627,7 +685,8 @@ pty_getproc(int fd) { #else static char * -pty_getproc(int fd, char *tty) { +pty_getproc(int fd, char *tty) +{ return NULL; } @@ -635,16 +694,18 @@ pty_getproc(int fd, char *tty) { #if defined(__APPLE__) static void -pty_posix_spawn(char** argv, char** env, +pty_posix_spawn(char **argv, char **env, const struct termios *termp, const struct winsize *winp, - int* master, - pid_t* pid, - int* err) { + int *master, + pid_t *pid, + int *err) +{ int low_fds[3]; size_t count = 0; - for (; count < 3; count++) { + for (; count < 3; count++) + { low_fds[count] = posix_openpt(O_RDWR); if (low_fds[count] >= STDERR_FILENO) break; @@ -655,12 +716,14 @@ pty_posix_spawn(char** argv, char** env, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSID; *master = posix_openpt(O_RDWR); - if (*master == -1) { + if (*master == -1) + { return; } int res = grantpt(*master) || unlockpt(*master); - if (res == -1) { + if (res == -1) + { return; } @@ -668,25 +731,31 @@ pty_posix_spawn(char** argv, char** env, int slave; char slave_pty_name[128]; res = ioctl(*master, TIOCPTYGNAME, slave_pty_name); - if (res == -1) { + if (res == -1) + { return; } slave = open(slave_pty_name, O_RDWR | O_NOCTTY); - if (slave == -1) { + if (slave == -1) + { return; } - if (termp) { + if (termp) + { res = tcsetattr(slave, TCSANOW, termp); - if (res == -1) { + if (res == -1) + { return; }; } - if (winp) { + if (winp) + { res = ioctl(slave, TIOCSWINSZ, winp); - if (res == -1) { + if (res == -1) + { return; } } @@ -702,7 +771,8 @@ pty_posix_spawn(char** argv, char** env, posix_spawnattr_t attrs; posix_spawnattr_init(&attrs); *err = posix_spawnattr_setflags(&attrs, flags); - if (*err != 0) { + if (*err != 0) + { goto done; } @@ -710,25 +780,29 @@ pty_posix_spawn(char** argv, char** env, /* Reset all signal the child to their default behavior */ sigfillset(&signal_set); *err = posix_spawnattr_setsigdefault(&attrs, &signal_set); - if (*err != 0) { + if (*err != 0) + { goto done; } /* Reset the signal mask for all signals */ sigemptyset(&signal_set); *err = posix_spawnattr_setsigmask(&attrs, &signal_set); - if (*err != 0) { + if (*err != 0) + { goto done; } - do { + do + { *err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env); } while (*err == EINTR); done: posix_spawn_file_actions_destroy(&acts); posix_spawnattr_destroy(&attrs); - for (; count > 0; count--) { + for (; count > 0; count--) + { close(low_fds[count]); } } diff --git a/addons/godot_xterm/native/src/pty_unix.h b/addons/godot_xterm/native/src/pty_unix.h index ffc75c0..4be8228 100644 --- a/addons/godot_xterm/native/src/pty_unix.h +++ b/addons/godot_xterm/native/src/pty_unix.h @@ -12,28 +12,25 @@ namespace godot { public: static Dictionary fork( - const String &p_file, - const PackedStringArray &p_args, - const PackedStringArray &p_env, - const String &p_cwd, - const int &p_cols, - const int &p_rows, - const int &p_uid, - const int &p_gid, - const bool &p_utf8, - const String &p_helper_path, - const Callable &p_on_exit - ); + const String &p_file, + const PackedStringArray &p_args, + const PackedStringArray &p_env, + const String &p_cwd, + const int &p_cols, + const int &p_rows, + const int &p_uid, + const int &p_gid, + const bool &p_utf8, + const String &p_helper_path, + const Callable &p_on_exit); static Dictionary open( - const int &p_cols, - const int &p_rows - ); + const int &p_cols, + const int &p_rows); static void resize( - const int &p_fd, - const int &p_cols, - const int &p_rows - ); + const int &p_fd, + const int &p_cols, + const int &p_rows); }; } // namespace godot diff --git a/addons/godot_xterm/native/src/pty_win.cpp b/addons/godot_xterm/native/src/pty_win.cpp new file mode 100644 index 0000000..4be8bef --- /dev/null +++ b/addons/godot_xterm/native/src/pty_win.cpp @@ -0,0 +1,443 @@ +/** + * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) + * Copyright (c) 2017, Daniel Imms (MIT License) + * Copyright (c) 2024, Leroy Hopson (MIT License) + * Copyright (c) 2024, Alexander Treml (MIT License) + * + * SPDX-License-Identifier: MIT + * + * pty.cc: + * This file is responsible for starting processes + * with pseudo-terminal file descriptors. + * + * See: + * conpty (https://github.com/microsoft/terminal/blob/main/samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp) + */ + +/** + * Includes + */ + +#if defined(_WIN32) && !defined(_PTY_DISABLED) + +#include "pty_win.h" + +#include +#include +#include + +#include +#include +#include + +#include + +using namespace godot; + +HRESULT CreatePseudoConsoleAndPipes(COORD, HPCON *, int &, int &); +HRESULT InitializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX *, HPCON); +void setup_exit_callback(Callable, int64_t); + +struct DelBuf +{ + int len; + DelBuf(int len) : len(len) {} + void operator()(char **p) + { + if (p == nullptr) + return; + for (int i = 0; i < len; i++) + free(p[i]); + delete[] p; + } +}; + +Dictionary PTYWin::fork( + const String &p_file, + const PackedStringArray &p_args, + const PackedStringArray &p_env, + const String &p_cwd, + const int &p_cols, + const int &p_rows, + const int &p_uid, + const int &p_gid, + const bool &p_utf8, + const String &p_helper_path, + const Callable &p_on_exit) +{ + Dictionary result; + + // file + std::string file = p_file.utf8().get_data(); + + // args + PackedStringArray argv_ = p_args; + + // env + PackedStringArray env_ = p_env; + int envc = env_.size(); + std::unique_ptr env_unique_ptr(new char *[envc + 1], DelBuf(envc + 1)); + char **env = env_unique_ptr.get(); + env[envc] = NULL; + for (int i = 0; i < envc; i++) + { + std::string pair = env_[i].utf8().get_data(); + env[i] = strdup(pair.c_str()); + } + + std::string cwd_ = p_cwd.utf8().get_data(); + + // Determine required size of Pseudo Console + COORD winp{}; + winp.X = p_cols; + winp.Y = p_rows; + + int uid = p_uid; + int gid = p_gid; + + std::string helper_path = p_helper_path.utf8().get_data(); + + // Build argv + int argc = argv_.size(); + int argl = argc + 2; + std::unique_ptr argv_unique_ptr(new char *[argl], DelBuf(argl)); + char **argv = argv_unique_ptr.get(); + argv[0] = strdup(file.c_str()); + argv[argl - 1] = NULL; + for (int i = 0; i < argc; i++) + { + std::string arg = argv_[i].utf8().get_data(); + argv[i + 1] = strdup(arg.c_str()); + } + + // Aggregate argv into command string + std::string cmd; + for (int i = 0; i < argl - 1; i++) + { + if (i > 0) + cmd += " "; + cmd += argv[i]; + } + LPSTR lpcmd = const_cast(cmd.c_str()); + + HRESULT ret{E_UNEXPECTED}; + HPCON hPC{INVALID_HANDLE_VALUE}; + + // Create the Pseudo Console and pipes to it + int fd{-1}; + int fd_out{-1}; + ret = CreatePseudoConsoleAndPipes(winp, &hPC, fd, fd_out); + + if (S_OK != ret) + { + result["error"] = ERR_CANT_FORK; + return result; + } + + // Initialize the necessary startup info struct + STARTUPINFOEX startupInfo{}; + if (S_OK != InitializeStartupInfoAttachedToPseudoConsole(&startupInfo, hPC)) + { + result["error"] = ERR_UNCONFIGURED; + return result; + } + + PROCESS_INFORMATION pi{}; + ret = CreateProcess( + NULL, // No module name - use Command Line + lpcmd, // Command Line + NULL, // Process handle not inheritable + NULL, // Thread handle not inheritable + FALSE, // Inherit handles + EXTENDED_STARTUPINFO_PRESENT, // Creation flags + NULL, // Use parent's environment block + NULL, // Use parent's starting directory + &startupInfo.StartupInfo, // Pointer to STARTUPINFO + &pi) // Pointer to PROCESS_INFORMATION + ? S_OK + : GetLastError(); + + result["fd"] = fd; + result["fd_out"] = fd_out; + result["pid"] = static_cast(pi.dwProcessId); + // TODO there is no equivalent to ptsname under windows. If this value is needed to support certain use cases, a workaround needs to be found + result["pty"] = "ConPTY"; + result["hpc"] = reinterpret_cast(hPC); + + // Set up process exit callback. + Callable cb = p_on_exit; + setup_exit_callback(cb, pi.dwProcessId); + result["error"] = OK; + + return result; +} + +Dictionary PTYWin::open( + const int &p_cols, + const int &p_rows) +{ + HRESULT ret{E_UNEXPECTED}; + Dictionary result; + result["error"] = FAILED; + + // TODO since the ConPTY API differs in many aspects from the unix PTY, this would need to look somewhat different + // Not currently supported because unsure about how the interface should look, and what the use cases are. + godot::UtilityFunctions::printerr("Not supported under windows!"); + + return result; +} + +void PTYWin::close(uint64_t hpc, int fd, int fd_out) +{ + ClosePseudoConsole(reinterpret_cast(hpc)); + + // TODO(ast) this causes assertion errors. Might be related to this: https://stackoverflow.com/questions/5984144/assertion-error-in-crt-calling-osfile-in-vs-2008 + // TODO(ast) leave for now and return when the linking issues are figured out (see SConstruct file) + // // Drain remaining data + // char drain_buf[4096]; + // int bytes_read = 0; + + // // Check if fd is valid before reading + // if (fd >= 0) { + // do { + // bytes_read = read(fd, drain_buf, sizeof(drain_buf)); + // } while (bytes_read > 0); + // godot::UtilityFunctions::print("Draining!"); + // } else { + // godot::UtilityFunctions::print("fd is not valid, skipping drain."); + // } + + // // Optionally close file descriptors if they are valid + // if (fd >= 0) _close(fd); + // if (fd_out >= 0) _close(fd_out); +} + +// TODO(ast) repeatedly resizing sometimes crashes the terminal +void PTYWin::resize( + int64_t p_hpc, + const int &p_cols, + const int &p_rows) +{ + COORD winp{}; + winp.X = p_cols; + winp.Y = p_rows; + + HPCON hpc = reinterpret_cast(p_hpc); + HRESULT hr = ResizePseudoConsole(hpc, winp); + if (FAILED(hr)) + { + DWORD err = GetLastError(); + godot::UtilityFunctions::printerr("ResizePseudoConsole failed. HRESULT: ", String::num_int64(hr), ", GetLastError: ", String::num_int64(err)); + } +} + +/** + * Derived from https://github.com/microsoft/terminal/blob/main/samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp + * Copyright (c) Microsoft Corporation (MIT License) + * + * SPDX-License-Identifier: MIT + * **/ +HRESULT CreatePseudoConsoleAndPipes(COORD size, HPCON *phPC, int &pFd, int &pFd_out) +{ + HRESULT hr{E_UNEXPECTED}; + + // Generate unique pipe names using process id and tick count + DWORD pid = GetCurrentProcessId(); + DWORD ticks = GetTickCount(); + wchar_t pipe_name_in[128]; + wchar_t pipe_name_out[128]; + swprintf(pipe_name_in, 128, L"\\\\.\\pipe\\godot_conpty_pipe_%lu_%lu_in", (unsigned long)pid, (unsigned long)ticks); + swprintf(pipe_name_out, 128, L"\\\\.\\pipe\\godot_conpty_pipe_%lu_%lu_out", (unsigned long)pid, (unsigned long)ticks); + + // Create bidirectional named pipes. Only one direction will be used on each. + // This is done to achieve compatibility with pty.cpp since uv_pipe_open only works on named pipes + HANDLE hPipeIn = INVALID_HANDLE_VALUE; + HANDLE hPipeOut = INVALID_HANDLE_VALUE; + HANDLE hPipeInFile = INVALID_HANDLE_VALUE; + HANDLE hPipeOutFile = INVALID_HANDLE_VALUE; + bool hPC_created = false; + + hPipeIn = CreateNamedPipeW( + pipe_name_in, + PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 1, // Max instances + 4096, 4096, // Out/in buffer size + 0, // Default timeout + NULL // Default security + ); + + if (hPipeIn == INVALID_HANDLE_VALUE) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto cleanup; + } + + hPipeOut = CreateNamedPipeW( + pipe_name_out, + PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 1, 4096, 4096, 0, NULL); + + if (hPipeOut == INVALID_HANDLE_VALUE) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto cleanup; + } + + // Connect to the named pipes + hPipeInFile = CreateFileW( + pipe_name_in, + GENERIC_READ | GENERIC_WRITE, + 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); + + if (hPipeInFile == INVALID_HANDLE_VALUE) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto cleanup; + } + + hPipeOutFile = CreateFileW( + pipe_name_out, + GENERIC_READ | GENERIC_WRITE, + 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); + + if (hPipeOutFile == INVALID_HANDLE_VALUE) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto cleanup; + } + + // Create the Pseudo Console + hr = CreatePseudoConsole(size, hPipeOut, hPipeIn, 0, phPC); + + if (FAILED(hr)) + { + goto cleanup; + } + hPC_created = true; + + // Convert HANDLE to C file descriptor + int fd = _open_osfhandle((intptr_t)hPipeInFile, 0); + if (fd == -1) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto cleanup; + } + + pFd = fd; + hPipeInFile = INVALID_HANDLE_VALUE; // Ownership transferred to fd + + fd = _open_osfhandle((intptr_t)hPipeOutFile, 0); + if (fd == -1) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto cleanup; + } + + pFd_out = fd; + hPipeOutFile = INVALID_HANDLE_VALUE; // Ownership transferred + + hr = S_OK; + +cleanup: + // Only close handles that are still valid and not transferred + if (hPipeIn != INVALID_HANDLE_VALUE) + CloseHandle(hPipeIn); + if (hPipeOut != INVALID_HANDLE_VALUE) + CloseHandle(hPipeOut); + if (hPipeInFile != INVALID_HANDLE_VALUE) + CloseHandle(hPipeInFile); + if (hPipeOutFile != INVALID_HANDLE_VALUE) + CloseHandle(hPipeOutFile); + if (FAILED(hr) && hPC_created && phPC) + ClosePseudoConsole(*phPC); + + return hr; +} + + +/** + * Copied from https://github.com/microsoft/terminal/blob/main/samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp + * Copyright (c) Microsoft Corporation (MIT License) + * + * SPDX-License-Identifier: MIT + * **/ +// Initializes the specified startup info struct with the required properties and +// updates its thread attribute list with the specified ConPTY handle +HRESULT InitializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX *pStartupInfo, HPCON hPC) +{ + HRESULT hr{E_UNEXPECTED}; + + if (pStartupInfo) + { + size_t attrListSize{}; + + pStartupInfo->StartupInfo.cb = sizeof(STARTUPINFOEX); + + // Get the size of the thread attribute list. + InitializeProcThreadAttributeList(NULL, 1, 0, &attrListSize); + + // Allocate a thread attribute list of the correct size + pStartupInfo->lpAttributeList = + reinterpret_cast(malloc(attrListSize)); + + // Initialize thread attribute list + if (pStartupInfo->lpAttributeList && InitializeProcThreadAttributeList(pStartupInfo->lpAttributeList, 1, 0, &attrListSize)) + { + // Set Pseudo Console attribute + hr = UpdateProcThreadAttribute( + pStartupInfo->lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + hPC, + sizeof(HPCON), + NULL, + NULL) + ? S_OK + : HRESULT_FROM_WIN32(GetLastError()); + } + else + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + } + return hr; +} + +static void await_exit(Callable cb, int64_t pid) +{ + DWORD exit_code = 0; + int signal_code = 0; + + HANDLE hProcess = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, static_cast(pid)); + if (hProcess != NULL) + { + WaitForSingleObject(hProcess, INFINITE); + GetExitCodeProcess(hProcess, &exit_code); + CloseHandle(hProcess); + cb.call_deferred(static_cast(exit_code), signal_code); + } + else + { + godot::UtilityFunctions::printerr("Could not open process!"); + } +} + +static void on_exit(int exit_code, int signal_code, Callable cb, Thread *thread) +{ + cb.call(exit_code, signal_code); + thread->wait_to_finish(); +} + +void setup_exit_callback(Callable cb, int64_t pid) +{ + Thread *thread = memnew(Thread); + + Callable exit_func = create_custom_callable_static_function_pointer(&on_exit).bind(cb, thread); + Callable thread_func = create_custom_callable_static_function_pointer(&await_exit).bind(exit_func, pid); + + thread->start(thread_func); +} + +#endif diff --git a/addons/godot_xterm/native/src/pty_win.h b/addons/godot_xterm/native/src/pty_win.h new file mode 100644 index 0000000..c62e1b0 --- /dev/null +++ b/addons/godot_xterm/native/src/pty_win.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Leroy Hopson +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +namespace godot +{ + class PTYWin + { + public: + static Dictionary fork( + const String &p_file, + const PackedStringArray &p_args, + const PackedStringArray &p_env, + const String &p_cwd, + const int &p_cols, + const int &p_rows, + const int &p_uid, + const int &p_gid, + const bool &p_utf8, + const String &p_helper_path, + const Callable &p_on_exit); + + static Dictionary open( + const int &p_cols, + const int &p_rows); + + static void resize( + int64_t p_hpc, + const int &p_cols, + const int &p_rows); + + static void close( + uint64_t hpc, + int fd, + int fd_out); + }; +} // namespace godot diff --git a/addons/godot_xterm/native/src/terminal.cpp b/addons/godot_xterm/native/src/terminal.cpp index d839ea6..4ecd56c 100644 --- a/addons/godot_xterm/native/src/terminal.cpp +++ b/addons/godot_xterm/native/src/terminal.cpp @@ -34,9 +34,9 @@ using namespace godot; void Terminal::_bind_methods() { - 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::VECTOR2I, "new_size"))); + 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::VECTOR2I, "new_size"))); ClassDB::bind_method(D_METHOD("get_cols"), &Terminal::get_cols); ClassDB::bind_method(D_METHOD("get_rows"), &Terminal::get_rows); @@ -146,11 +146,13 @@ int Terminal::get_rows() const return rows; } -Vector2i Terminal::get_cursor_pos() const { +Vector2i Terminal::get_cursor_pos() const +{ return Vector2i(tsm_screen_get_cursor_x(screen), tsm_screen_get_cursor_y(screen)); } -Vector2 Terminal::get_cell_size() const { +Vector2 Terminal::get_cell_size() const +{ return cell_size; } @@ -181,7 +183,8 @@ String Terminal::write(const Variant data) ERR_FAIL_V_MSG("", "Data must be a String or PackedByteArray."); } - if (bytes.is_empty()) return ""; + if (bytes.is_empty()) + return ""; response.clear(); tsm_vte_input(vte, (char *)bytes.ptr(), bytes.size()); @@ -190,7 +193,8 @@ String Terminal::write(const Variant data) return response.get_string_from_utf8(); } -void Terminal::_gui_input(const Ref &event) { +void Terminal::_gui_input(const Ref &event) +{ _handle_key_input(event); _handle_selection(event); _handle_mouse_wheel(event); @@ -235,13 +239,15 @@ void Terminal::_write_cb(tsm_vte *vte, const char *u8, size_t len, void *data) { Terminal *term = static_cast(data); - if (len > 0) { + if (len > 0) + { PackedByteArray data; data.resize(len); memcpy(data.ptrw(), u8, len); term->response.append_array(data); - if (term->last_input_event_key.is_valid()) { + if (term->last_input_event_key.is_valid()) + { term->emit_signal("key_pressed", data.get_string_from_utf8(), term->last_input_event_key); term->last_input_event_key.unref(); } @@ -263,10 +269,11 @@ int Terminal::_draw_cb(struct tsm_screen *con, { Terminal *term = static_cast(data); - if (age != 0 && age <= term->framebuffer_age) return OK; + if (age != 0 && age <= term->framebuffer_age) + return OK; if (width < 1) - { // No foreground or background to draw. + { // No foreground or background to draw. return OK; } @@ -276,20 +283,22 @@ int Terminal::_draw_cb(struct tsm_screen *con, attr_flags |= AttrFlag::INVERSE; if (attr->blink) attr_flags |= AttrFlag::BLINK; - if (term->cursor_position.x == posx && term->cursor_position.y == posy) { + if (term->cursor_position.x == posx && term->cursor_position.y == posy) + { attr_flags |= AttrFlag::CURSOR; } // Collect 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); + ? 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); + ? term->palette[attr->bccode] + : Color(attr->br / 255.0f, attr->bg / 255.0f, attr->bb / 255.0f); - if (attr->inverse && term->inverse_mode == InverseMode::INVERSE_MODE_SWAP) { + if (attr->inverse && term->inverse_mode == InverseMode::INVERSE_MODE_SWAP) + { std::swap(fgcol.r, bgcol.r); std::swap(fgcol.g, bgcol.g); std::swap(fgcol.b, bgcol.b); @@ -300,7 +309,8 @@ int Terminal::_draw_cb(struct tsm_screen *con, bgcol.a = 0; // Update images (accounting for ultra-wide characters). - for (int i = 0; i < width && (posx + i) < term->cols; i++) { + for (int i = 0; i < width && (posx + i) < term->cols; i++) + { term->back_image->set_pixel(posx + i, posy, bgcol); term->attr_image->set_pixel(posx + i, posy, Color(attr_flags / 255.0f, 0, 0, 0)); } @@ -312,7 +322,7 @@ int Terminal::_draw_cb(struct tsm_screen *con, term->rs->canvas_item_add_rect(term->char_canvas_item, cell_rect, Color(1, 1, 1, 0)); if (len < 1) - { // No foreground to draw. + { // No foreground to draw. return OK; } @@ -323,8 +333,7 @@ int Terminal::_draw_cb(struct tsm_screen *con, Vector2i(cell_position.x, cell_position.y + term->font_offset), static_cast(*ch), term->font_size, - fgcol - ); + fgcol); return OK; } @@ -333,7 +342,8 @@ void Terminal::_bell_cb(struct tsm_vte *vte, void *data) { Terminal *term = static_cast(data); - if (!term->bell_muted && term->bell_timer->is_stopped()) { + if (!term->bell_muted && term->bell_timer->is_stopped()) + { term->emit_signal("bell"); if (term->bell_cooldown > 0) @@ -393,12 +403,15 @@ bool Terminal::_set(const StringName &p_name, const Variant &p_value) return false; } -bool Terminal::_get(const StringName &p_name, Variant &r_value) { - if (p_name == String("cols")) { +bool Terminal::_get(const StringName &p_name, Variant &r_value) +{ + if (p_name == String("cols")) + { r_value = cols; return true; } - if (p_name == String("rows")) { + if (p_name == String("rows")) + { r_value = rows; return true; } @@ -485,20 +498,24 @@ void Terminal::update_sizes(bool force) void Terminal::set_shader_parameters(const String ¶m, const Variant &value) { - if (param.is_empty()) { + if (param.is_empty()) + { set_shader_parameters("cols", cols); set_shader_parameters("rows", rows); set_shader_parameters("size", size); set_shader_parameters("cell_size", cell_size); set_shader_parameters("grid_size", Vector2(cols * cell_size.x, rows * cell_size.y)); - } else { + } + else + { back_material->set_shader_parameter(param, value); fore_material->set_shader_parameter(param, value); } } -void Terminal::initialize_rendering() { - ResourceLoader* rl = ResourceLoader::get_singleton(); +void Terminal::initialize_rendering() +{ + ResourceLoader *rl = ResourceLoader::get_singleton(); rs = RenderingServer::get_singleton(); attr_texture.instantiate(); @@ -542,7 +559,7 @@ void Terminal::initialize_rendering() { canvas = rs->canvas_create(); rs->canvas_item_set_parent(char_canvas_item, canvas); - viewport = rs-> viewport_create(); + viewport = rs->viewport_create(); rs->viewport_attach_canvas(viewport, canvas); rs->viewport_set_disable_3d(viewport, true); rs->viewport_set_transparent_background(viewport, true); @@ -563,16 +580,19 @@ void Terminal::initialize_rendering() { rs->connect("frame_post_draw", Callable(this, "_on_frame_post_draw")); } -void Terminal::update_theme() { +void Terminal::update_theme() +{ // Update colors. palette.resize(TSM_COLOR_NUM); - for (int i = 0; i < TSM_COLOR_NUM; i++) { + 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])); } // Update fonts. - for (int i = FontType::NORMAL; i <= FontType::BOLD_ITALICS; i++) { + for (int i = FontType::NORMAL; i <= FontType::BOLD_ITALICS; i++) + { FontType type = static_cast(i); fonts[type] = has_theme_font(FONT_TYPES[type]) ? get_theme_font(FONT_TYPES[type]) : get_theme_font(FONT_TYPES[FontType::NORMAL]); } @@ -581,25 +601,30 @@ void Terminal::update_theme() { style_normal = get_theme_stylebox("normal"); style_focus = get_theme_stylebox("focus"); - if (dynamic_cast(style_normal.ptr()) != nullptr) { + if (dynamic_cast(style_normal.ptr()) != nullptr) + { // Blend the background color with the style box's background color to get the "true" background color. Color style_background_color = style_normal->get("bg_color"); - palette[TSM_COLOR_BACKGROUND] = style_background_color.blend(palette[TSM_COLOR_BACKGROUND]); + palette[TSM_COLOR_BACKGROUND] = style_background_color.blend(palette[TSM_COLOR_BACKGROUND]); } back_material->set_shader_parameter("background_color", palette[TSM_COLOR_BACKGROUND]); refresh(); } -void Terminal::_on_frame_post_draw() { - if (redraw_requested) { +void Terminal::_on_frame_post_draw() +{ + if (redraw_requested) + { queue_redraw(); redraw_requested = false; } } -void Terminal::draw_screen() { - if (framebuffer_age == 0) { +void Terminal::draw_screen() +{ + if (framebuffer_age == 0) + { Rect2 rect = Rect2(Vector2(), size); rs->viewport_set_clear_mode(viewport, RenderingServer::ViewportClearMode::VIEWPORT_CLEAR_ONLY_NEXT_FRAME); @@ -628,12 +653,14 @@ void Terminal::draw_screen() { back_texture->update(back_image); } -void Terminal::refresh() { +void Terminal::refresh() +{ framebuffer_age = 0; queue_redraw(); } -void Terminal::cleanup_rendering() { +void Terminal::cleanup_rendering() +{ // StyleBox. rs->free_rid(style_canvas_item); @@ -649,18 +676,22 @@ void Terminal::cleanup_rendering() { rs->free_rid(char_shader); } -void Terminal::set_bell_muted(const bool muted) { +void Terminal::set_bell_muted(const bool muted) +{ bell_muted = muted; } -bool Terminal::get_bell_muted() const { +bool Terminal::get_bell_muted() const +{ return bell_muted; } -void Terminal::set_bell_cooldown(const double time) { +void Terminal::set_bell_cooldown(const double time) +{ bell_cooldown = time; - if (!bell_timer->is_stopped()) { + if (!bell_timer->is_stopped()) + { bell_timer->stop(); double remaining_time = std::max(0.0, bell_cooldown - bell_timer->get_time_left()); @@ -669,7 +700,8 @@ void Terminal::set_bell_cooldown(const double time) { } } -double Terminal::get_bell_cooldown() const { +double Terminal::get_bell_cooldown() const +{ return bell_cooldown; } @@ -695,25 +727,28 @@ double Terminal::get_blink_off_time() const return blink_off_time; } -void Terminal::clear() { - // Resize the terminal to a single row, forcing content above in to the scrollback buffer. - tsm_screen_resize(screen, cols, 1); +void Terminal::clear() +{ + // Resize the terminal to a single row, forcing content above in to the scrollback buffer. + tsm_screen_resize(screen, cols, 1); - // Clear the scrollback buffer (hence clearing the content that was above). - tsm_screen_clear_sb(screen); + // Clear the scrollback buffer (hence clearing the content that was above). + tsm_screen_clear_sb(screen); - // Resize the screen to its original size. - tsm_screen_resize(screen, cols, rows); + // Resize the screen to its original size. + tsm_screen_resize(screen, cols, rows); - refresh(); + refresh(); } -String Terminal::_copy_screen(ScreenCopyFunction func) { +String Terminal::_copy_screen(ScreenCopyFunction func) +{ char *out; PackedByteArray data; data.resize(std::max(func(screen, &out), 0)); - if (data.size() > 0) { + if (data.size() > 0) + { memcpy(data.ptrw(), out, data.size()); std::free(out); } @@ -721,16 +756,20 @@ String Terminal::_copy_screen(ScreenCopyFunction func) { return data.get_string_from_utf8(); } -void Terminal::select(const int p_from_line, const int p_from_column, const int p_to_line, const int p_to_column) { +void Terminal::select(const int p_from_line, const int p_from_column, const int p_to_line, const int p_to_column) +{ int from_line = std::clamp((int)p_from_line, 0, (int)rows); int from_column = std::clamp((int)p_from_column, 0, (int)cols); int to_line = std::clamp((int)p_to_line, 0, (int)rows); int to_column = std::clamp((int)p_to_column, 0, (int)cols); - if (from_line > to_line) { + if (from_line > to_line) + { std::swap(to_line, from_line); std::swap(to_column, from_column); - } else if ((from_line == to_line) && (from_column > to_column)) { + } + else if ((from_line == to_line) && (from_column > to_column)) + { std::swap(to_column, from_column); } @@ -742,34 +781,40 @@ void Terminal::select(const int p_from_line, const int p_from_column, const int String selection = copy_selection(); - #if defined(__linux__) +#if defined(__linux__) if (copy_on_selection) DisplayServer::get_singleton()->clipboard_set_primary(selection); - #endif +#endif - if (selection.length() > 0) { + if (selection.length() > 0) + { selecting = true; queue_redraw(); } } -String Terminal::copy_all() { +String Terminal::copy_all() +{ return _copy_screen(&tsm_screen_copy_all); } -String Terminal::copy_selection() { +String Terminal::copy_selection() +{ return _copy_screen(&tsm_screen_selection_copy); } -void Terminal::set_copy_on_selection(const bool p_enabled) { +void Terminal::set_copy_on_selection(const bool p_enabled) +{ copy_on_selection = p_enabled; } -bool Terminal::get_copy_on_selection() const { +bool Terminal::get_copy_on_selection() const +{ return copy_on_selection; } -void Terminal::set_inverse_mode(const int mode) { +void Terminal::set_inverse_mode(const int mode) +{ inverse_mode = static_cast(mode); bool inverse_enabled = inverse_mode == InverseMode::INVERSE_MODE_INVERT; @@ -778,11 +823,13 @@ void Terminal::set_inverse_mode(const int mode) { refresh(); } -int Terminal::get_inverse_mode() const { +int Terminal::get_inverse_mode() const +{ return static_cast(inverse_mode); } -void Terminal::initialize_input() { +void Terminal::initialize_input() +{ selecting = false; selection_mode = SelectionMode::NONE; selection_timer = memnew(Timer); @@ -791,129 +838,144 @@ void Terminal::initialize_input() { add_child(selection_timer, false, INTERNAL_MODE_FRONT); } -void Terminal::_handle_key_input(Ref event) { - if (!event.is_valid() || !event->is_pressed()) - return; +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; + 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_SHIFT_MASK; - if (event->is_ctrl_pressed()) - mods |= TSM_CONTROL_MASK; - if (event->is_shift_pressed()) - mods |= TSM_SHIFT_MASK; + 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; + 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); + 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); - 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; - default: - 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); - 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); - 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); - queue_redraw(); - selection_timer->start(); - } - return; - } -} - -void Terminal::_on_selection_held() { - if (!(Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) || selection_mode == SelectionMode::NONE) { - #if defined(__linux__) - if (copy_on_selection) { - DisplayServer::get_singleton()->clipboard_set_primary(copy_selection()); + // 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); + queue_redraw(); } - #endif - selection_timer->stop(); - return; - } + // 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(); +} - Vector2 target = get_local_mouse_position() / cell_size; - tsm_screen_selection_target(screen, target.x, target.y); - queue_redraw(); - selection_timer->start(); +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; + default: + 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); + 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); + 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); + queue_redraw(); + selection_timer->start(); + } + return; + } +} + +void Terminal::_on_selection_held() +{ + if (!(Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) || selection_mode == SelectionMode::NONE) + { +#if defined(__linux__) + if (copy_on_selection) + { + DisplayServer::get_singleton()->clipboard_set_primary(copy_selection()); + } +#endif + + selection_timer->stop(); + return; + } + + Vector2 target = get_local_mouse_position() / cell_size; + tsm_screen_selection_target(screen, target.x, target.y); + queue_redraw(); + selection_timer->start(); } // Add default theme items for the "Terminal" theme type if they don't exist. // These defaults match Godot's built-in default theme (note: this is different from the default editor theme). -void Terminal::set_default_theme_items() { +void Terminal::set_default_theme_items() +{ Ref default_theme = ThemeDB::get_singleton()->get_default_theme(); - if (default_theme->get_type_list().has("Terminal")) return; + if (default_theme->get_type_list().has("Terminal")) + return; // As a workaround, create a new theme and then merge it with the default theme at the end. // See: https://github.com/godotengine/godot-cpp/issues/1332#issuecomment-2041060614. @@ -927,16 +989,16 @@ void Terminal::set_default_theme_items() { // Default ANSI colors based on xterm defaults: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors. // Some discussion about the slight difference of the blue colors: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=241717. - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_0_color" , "Terminal", Color::hex(0x000000FF)); // Black - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_1_color" , "Terminal", Color::hex(0xCD0000FF)); // Red - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_2_color" , "Terminal", Color::hex(0x00CD00FF)); // Green - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_3_color" , "Terminal", Color::hex(0xCDCD00FF)); // Yellow - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_4_color" , "Terminal", Color::hex(0x0000EEFF)); // Blue - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_5_color" , "Terminal", Color::hex(0xCD00CDFF)); // Magenta - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_6_color" , "Terminal", Color::hex(0x00CDCDFF)); // Cyan - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_7_color" , "Terminal", Color::hex(0xE5E5E5FF)); // White - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_8_color" , "Terminal", Color::hex(0x7F7F7FFF)); // Bright Black (Gray) - custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_9_color" , "Terminal", Color::hex(0xFF0000FF)); // Bright Red + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_0_color", "Terminal", Color::hex(0x000000FF)); // Black + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_1_color", "Terminal", Color::hex(0xCD0000FF)); // Red + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_2_color", "Terminal", Color::hex(0x00CD00FF)); // Green + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_3_color", "Terminal", Color::hex(0xCDCD00FF)); // Yellow + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_4_color", "Terminal", Color::hex(0x0000EEFF)); // Blue + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_5_color", "Terminal", Color::hex(0xCD00CDFF)); // Magenta + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_6_color", "Terminal", Color::hex(0x00CDCDFF)); // Cyan + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_7_color", "Terminal", Color::hex(0xE5E5E5FF)); // White + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_8_color", "Terminal", Color::hex(0x7F7F7FFF)); // Bright Black (Gray) + custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_9_color", "Terminal", Color::hex(0xFF0000FF)); // Bright Red custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_10_color", "Terminal", Color::hex(0x00FF00FF)); // Bright Green custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_11_color", "Terminal", Color::hex(0xFFFF00FF)); // Bright Yellow custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_12_color", "Terminal", Color::hex(0x5C5CFFFF)); // Bright Blue @@ -945,11 +1007,13 @@ void Terminal::set_default_theme_items() { custom_theme->set_theme_item(Theme::DATA_TYPE_COLOR, "ansi_15_color", "Terminal", Color::hex(0xFFFFFFFF)); // Bright White // No monospaced font in the default theme, so try to import our own. Will default to a non-monospace font otherwise. - ResourceLoader* rl = ResourceLoader::get_singleton(); + ResourceLoader *rl = ResourceLoader::get_singleton(); String const font_path = "res://addons/godot_xterm/themes/fonts/regular.tres"; - if (rl->exists(font_path)) { + if (rl->exists(font_path)) + { Ref default_font = rl->load(font_path); - for (int i = FontType::NORMAL; i <= FontType::BOLD_ITALICS; i++) { + for (int i = FontType::NORMAL; i <= FontType::BOLD_ITALICS; i++) + { FontType type = static_cast(i); custom_theme->set_theme_item(Theme::DATA_TYPE_FONT, FONT_TYPES[type], "Terminal", default_font); } diff --git a/addons/godot_xterm/native/src/terminal.h b/addons/godot_xterm/native/src/terminal.h index 6bb7db4..ec4d363 100644 --- a/addons/godot_xterm/native/src/terminal.h +++ b/addons/godot_xterm/native/src/terminal.h @@ -27,7 +27,8 @@ namespace godot private: typedef std::map, uint32_t> KeyMap; - enum FontType { + enum FontType + { NORMAL, BOLD, ITALICS, @@ -37,6 +38,7 @@ namespace godot static const char *COLOR_NAMES[18]; static const char *FONT_TYPES[4]; static const KeyMap KEY_MAP; + public: enum AttrFlag { @@ -45,7 +47,8 @@ namespace godot CURSOR = 1 << 2, }; - enum InverseMode { + enum InverseMode + { INVERSE_MODE_INVERT, INVERSE_MODE_SWAP, }; @@ -89,6 +92,7 @@ namespace godot String write(const Variant data); void _gui_input(const Ref &event) override; + protected: static void _bind_methods(); @@ -123,7 +127,7 @@ namespace godot // This can be useful in cases where the bell character is being written too // frequently such as `while true; do echo -e "\a"; done`. double bell_cooldown; - Timer* bell_timer; + Timer *bell_timer; static void _bell_cb(struct tsm_vte *vte, void *data); static int _draw_cb(struct tsm_screen *con, uint64_t id, const uint32_t *ch, @@ -184,14 +188,18 @@ namespace godot void _handle_mouse_wheel(Ref event); - enum SelectionMode { NONE, POINTER }; + enum SelectionMode + { + NONE, + POINTER + }; bool selecting = false; SelectionMode selection_mode = SelectionMode::NONE; Timer *selection_timer; void _handle_selection(Ref event); void _on_selection_held(); - typedef std::function ScreenCopyFunction; + typedef std::function ScreenCopyFunction; String _copy_screen(ScreenCopyFunction func); void set_default_theme_items(); diff --git a/addons/godot_xterm/themes/default_green.tres b/addons/godot_xterm/themes/default_green.tres new file mode 100644 index 0000000..1e15821 --- /dev/null +++ b/addons/godot_xterm/themes/default_green.tres @@ -0,0 +1,58 @@ +[gd_resource type="Theme" load_steps=4 format=3 uid="uid://dtpro3m7sdgvg"] + +[ext_resource type="Script" path="res://addons/godot_xterm/resources/xrdb_theme.gd" id="1"] +[ext_resource type="FontFile" uid="uid://c51gnbjamppg" path="res://addons/godot_xterm/themes/fonts/jet_brains_mono/jet_brains_mono_nl-regular-2.304.ttf" id="2"] + +[sub_resource type="FontFile" id="FontFile_tdf0u"] +fallbacks = Array[Font]([ExtResource("2")]) +cache/0/16/0/ascent = 0.0 +cache/0/16/0/descent = 0.0 +cache/0/16/0/underline_position = 0.0 +cache/0/16/0/underline_thickness = 0.0 +cache/0/16/0/scale = 1.0 +cache/0/16/0/kerning_overrides/16/0 = Vector2(0, 0) +cache/0/16/0/kerning_overrides/11/0 = Vector2(0, 0) +cache/0/16/0/kerning_overrides/14/0 = Vector2(0, 0) +cache/0/11/0/ascent = 0.0 +cache/0/11/0/descent = 0.0 +cache/0/11/0/underline_position = 0.0 +cache/0/11/0/underline_thickness = 0.0 +cache/0/11/0/scale = 1.0 +cache/0/11/0/kerning_overrides/16/0 = Vector2(0, 0) +cache/0/11/0/kerning_overrides/11/0 = Vector2(0, 0) +cache/0/11/0/kerning_overrides/14/0 = Vector2(0, 0) +cache/0/14/0/ascent = 0.0 +cache/0/14/0/descent = 0.0 +cache/0/14/0/underline_position = 0.0 +cache/0/14/0/underline_thickness = 0.0 +cache/0/14/0/scale = 1.0 +cache/0/14/0/kerning_overrides/16/0 = Vector2(0, 0) +cache/0/14/0/kerning_overrides/11/0 = Vector2(0, 0) +cache/0/14/0/kerning_overrides/14/0 = Vector2(0, 0) + +[resource] +Terminal/colors/ansi_0_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_10_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_11_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_12_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_13_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_14_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_15_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_1_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_2_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_3_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_4_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_5_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_6_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_7_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_8_color = Color(0, 1, 0.4, 1) +Terminal/colors/ansi_9_color = Color(0, 1, 0.4, 1) +Terminal/colors/background_color = Color(0.156863, 0.156863, 0.156863, 1) +Terminal/colors/cursor = Color(0, 1, 0.4, 1) +Terminal/colors/foreground_color = Color(0, 1, 0.4, 1) +Terminal/font_sizes/font_size = 14 +Terminal/fonts/bold_font = null +Terminal/fonts/bold_italics_font = null +Terminal/fonts/italics_font = null +Terminal/fonts/normal_font = SubResource("FontFile_tdf0u") +script = ExtResource("1") diff --git a/addons/godot_xterm/themes/default_white.tres b/addons/godot_xterm/themes/default_white.tres new file mode 100644 index 0000000..a6cca3c --- /dev/null +++ b/addons/godot_xterm/themes/default_white.tres @@ -0,0 +1,26 @@ +[gd_resource type="Theme" load_steps=3 format=3 uid="uid://0gk8swmcldbg"] + +[ext_resource type="FontVariation" uid="uid://vmgmcu8gc6nt" path="res://addons/godot_xterm/themes/fonts/regular.tres" id="1_aigbn"] +[ext_resource type="StyleBox" uid="uid://cxaclm5pavuv6" path="res://themes/normal.stylebox" id="1_bj7pu"] + +[resource] +default_font = ExtResource("1_aigbn") +Terminal/colors/ansi_0_color = Color(0, 0, 0, 1) +Terminal/colors/ansi_10_color = Color(0.258824, 1, 0.760784, 1) +Terminal/colors/ansi_11_color = Color(1, 0.929412, 0.631373, 1) +Terminal/colors/ansi_12_color = Color(0.341176, 0.701961, 1, 1) +Terminal/colors/ansi_13_color = Color(0.639216, 0.639216, 0.960784, 1) +Terminal/colors/ansi_14_color = Color(0.4, 0.901961, 1, 1) +Terminal/colors/ansi_15_color = Color(1, 1, 1, 1) +Terminal/colors/ansi_1_color = Color(1, 0.470588, 0.419608, 1) +Terminal/colors/ansi_2_color = Color(0.388235, 0.760784, 0.34902, 1) +Terminal/colors/ansi_3_color = Color(0.980392, 0.890196, 0.270588, 1) +Terminal/colors/ansi_4_color = Color(0.0784314, 0.490196, 0.980392, 1) +Terminal/colors/ansi_5_color = Color(1, 0.54902, 0.8, 1) +Terminal/colors/ansi_6_color = Color(0.560784, 1, 0.858824, 1) +Terminal/colors/ansi_7_color = Color(0.803922, 0.811765, 0.823529, 0.501961) +Terminal/colors/ansi_8_color = Color(0.211765, 0.239216, 0.290196, 1) +Terminal/colors/ansi_9_color = Color(1, 0.439216, 0.521569, 1) +Terminal/colors/foreground_color = Color(0.803922, 0.811765, 0.823529, 1) +Terminal/font_sizes/font_size = 14 +Terminal/styles/normal = ExtResource("1_bj7pu") diff --git a/examples/menu/menu.gd b/examples/menu/menu.gd index 732d9f1..8c9d1c3 100644 --- a/examples/menu/menu.gd +++ b/examples/menu/menu.gd @@ -145,17 +145,6 @@ func _on_Terminal_key_pressed(data: String, event: InputEventKey) -> void: $Terminal.grab_focus() scene.queue_free() "Terminal": - if OS.get_name() == "Windows": - OS.call_deferred( - "alert", - ( - "Psuedoterminal node currently" - + " uses pty.h but needs to use either winpty or conpty" - + " to work on Windows." - ), - "Terminal not Supported on Windows" - ) - return var scene = item.scene.instantiate() var pty = scene if OS.has_feature("web") else scene.get_node("PTY") add_child(scene) diff --git a/examples/menu/menu.tscn b/examples/menu/menu.tscn index a7d51a4..35229ed 100644 --- a/examples/menu/menu.tscn +++ b/examples/menu/menu.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=3 format=3 uid="uid://brjrtf5fpptw8"] [ext_resource type="Script" path="res://examples/menu/menu.gd" id="2"] -[ext_resource type="Theme" uid="uid://0gk8swmcldbg" path="res://themes/demo.tres" id="2_7f2wl"] +[ext_resource type="Theme" uid="uid://0gk8swmcldbg" path="res://themes/demo.tres" id="2_pr2sv"] [node name="Menu" type="Control"] layout_mode = 3 @@ -19,4 +19,4 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -theme = ExtResource("2_7f2wl") +theme = ExtResource("2_pr2sv") diff --git a/examples/terminal/terminal.gd b/examples/terminal/terminal.gd index bfc118f..65dd957 100644 --- a/examples/terminal/terminal.gd +++ b/examples/terminal/terminal.gd @@ -2,6 +2,5 @@ extends Terminal @onready var pty = $PTY - func _ready(): - pty.fork(OS.get_environment("SHELL")) + pty.fork() diff --git a/project.godot b/project.godot index d19f58c..a09b361 100644 --- a/project.godot +++ b/project.godot @@ -21,7 +21,7 @@ window/vsync/use_vsync=false [editor_plugins] -enabled=PackedStringArray("res://addons/godot_xterm/plugin.cfg", "res://addons/zylann.editor_debugger/plugin.cfg") +enabled=PackedStringArray("res://addons/godot_xterm/plugin.cfg") [rendering] diff --git a/test/integration/terminal.test.gd b/test/integration/terminal.test.gd index 83219c2..9ab14eb 100644 --- a/test/integration/terminal.test.gd +++ b/test/integration/terminal.test.gd @@ -10,6 +10,7 @@ func before_each(): func test_bell() -> void: + watch_signals(terminal) terminal.bell_cooldown = 0 terminal.write(char(7)) terminal.write(char(0x07)) @@ -42,8 +43,8 @@ class TestTheme: const TestScene := preload("../scenes/theme.tscn") - const default_theme := preload("res://addons/godot_xterm/themes/default.tres") - const alt_theme := preload("res://addons/godot_xterm/themes/default_light.tres") + const default_theme := preload("res://addons/godot_xterm/themes/default_green.tres") + const alt_theme := preload("res://addons/godot_xterm/themes/default_white.tres") const COLORS := [ "ansi_0_color", diff --git a/test/integration/uv_utils.test.gd b/test/integration/uv_utils.test.gd deleted file mode 100644 index 6c8b757..0000000 --- a/test/integration/uv_utils.test.gd +++ /dev/null @@ -1,29 +0,0 @@ -extends "res://addons/gut/test.gd" - -const EMPTY_VAR = "GODOT_XTERM_TEST_EMPTY_ENV_VAR" -const TEST_VAR = "GODOT_XTERM_TEST_ENV_VAR" -const TEST_VAL = "TEST123" - -var env: Dictionary - - -func before_each(): - OS.set_environment(EMPTY_VAR, "") - OS.set_environment(TEST_VAR, TEST_VAL) - env = LibuvUtils.get_os_environ() - - -func test_has_empty_var(): - assert_has(env, EMPTY_VAR) - - -func test_empty_var_has_empty_val(): - assert_eq(env[EMPTY_VAR], "") - - -func test_has_test_var(): - assert_has(env, TEST_VAR) - - -func test_test_var_has_correct_val(): - assert_eq(env[TEST_VAR], TEST_VAL) diff --git a/test/test_nix.gd b/test/test_nix.gd index 2fee26b..7bd064c 100644 --- a/test/test_nix.gd +++ b/test/test_nix.gd @@ -96,7 +96,7 @@ func test_emits_exit_code_on_failure(): func test_emits_exited_on_kill(): subject.call("fork", "yes") await wait_frames(1) - subject.call_deferred("kill", PTY.SIGNAL_SIGKILL) + subject.call_deferred("kill", PTY.IPCSIGNAL_SIGKILL) await wait_for_signal(subject.exited, 1) assert_signal_emitted(subject, "exited") @@ -104,9 +104,9 @@ func test_emits_exited_on_kill(): func test_emits_exited_with_signal(): subject.call("fork", "yes") await wait_frames(1) - subject.call_deferred("kill", PTY.SIGNAL_SIGSEGV) + subject.call_deferred("kill", PTY.IPCSIGNAL_SIGSEGV) await wait_for_signal(subject.exited, 1) - assert_signal_emitted_with_parameters(subject, "exited", [0, PTY.SIGNAL_SIGSEGV]) + assert_signal_emitted_with_parameters(subject, "exited", [0, PTY.IPCSIGNAL_SIGSEGV]) # Run the same tests, but with use_threads = false. @@ -189,7 +189,7 @@ class TestPTYSize: await wait_for_signal(subject.data_received, 1) func after_each(): - subject.call_deferred("kill", PTY.SIGNAL_SIGHUP) + subject.call_deferred("kill", PTY.IPCSIGNAL_SIGHUP) await wait_for_signal(subject.exited, 1) func test_pty_default_size(): diff --git a/test/test_pty.gd b/test/test_pty.gd index ee35ead..0b5a153 100644 --- a/test/test_pty.gd +++ b/test/test_pty.gd @@ -64,18 +64,18 @@ class TestInterface: # Enums. func test_has_enum_signal(): - assert_eq(described_class.SIGNAL_SIGHUP, 1) - assert_eq(described_class.SIGNAL_SIGINT, 2) - assert_eq(described_class.SIGNAL_SIGQUIT, 3) - assert_eq(described_class.SIGNAL_SIGILL, 4) - assert_eq(described_class.SIGNAL_SIGTRAP, 5) - assert_eq(described_class.SIGNAL_SIGABRT, 6) - assert_eq(described_class.SIGNAL_SIGFPE, 8) - assert_eq(described_class.SIGNAL_SIGKILL, 9) - assert_eq(described_class.SIGNAL_SIGSEGV, 11) - assert_eq(described_class.SIGNAL_SIGPIPE, 13) - assert_eq(described_class.SIGNAL_SIGALRM, 14) - assert_eq(described_class.SIGNAL_SIGTERM, 15) + assert_eq(described_class.IPCSIGNAL_SIGHUP, 1) + assert_eq(described_class.IPCSIGNAL_SIGINT, 2) + assert_eq(described_class.IPCSIGNAL_SIGQUIT, 3) + assert_eq(described_class.IPCSIGNAL_SIGILL, 4) + assert_eq(described_class.IPCSIGNAL_SIGTRAP, 5) + assert_eq(described_class.IPCSIGNAL_SIGABRT, 6) + assert_eq(described_class.IPCSIGNAL_SIGFPE, 8) + assert_eq(described_class.IPCSIGNAL_SIGKILL, 9) + assert_eq(described_class.IPCSIGNAL_SIGSEGV, 11) + assert_eq(described_class.IPCSIGNAL_SIGPIPE, 13) + assert_eq(described_class.IPCSIGNAL_SIGALRM, 14) + assert_eq(described_class.IPCSIGNAL_SIGTERM, 15) ## Other tests. diff --git a/test/unit/pty.test.gd b/test/unit/pty.test.gd index b05d0a3..cdbd7bb 100644 --- a/test/unit/pty.test.gd +++ b/test/unit/pty.test.gd @@ -1,23 +1,14 @@ extends "res://addons/gut/test.gd" - -class MockPTY: - extends "res://addons/godot_xterm/nodes/pty/pty_native.gd" - - func write(data): - emit_signal("data_received", data) - - class BaseTest: extends "res://addons/gut/test.gd" var pty - var mock_pty_native: MockPTY + var mock_pty_native: PTY func before_each(): pty = add_child_autofree(PTY.new()) - mock_pty_native = autofree(MockPTY.new()) - pty._pty_native = mock_pty_native + mock_pty_native = autofree(PTY.new()) watch_signals(mock_pty_native) @@ -70,33 +61,17 @@ class TestPTYInterfaceGodotXterm2_0_0: func test_has_signal_exited(): assert_has_signal(pty, "exited") - # NOTE: This differs from the GodotXterm 2.x API which uses Signal rather than IPCSignal. + # In Godot 4.x, enums are no longer dictionaries and thus need to be inspected individually. func test_has_enum_Signal(): - assert_true("IPCSignal" in pty, "Expected pty to have enum IPCSignal.") - assert_typeof(pty.IPCSignal, typeof(Dictionary())) - var signals = { - SIGHUP = 1, - SIGINT = 2, - SIGQUIT = 3, - SIGILL = 4, - SIGTRAP = 5, - SIGABRT = 6, - SIGFPE = 8, - SIGKILL = 9, - SIGSEGV = 11, - SIGPIPE = 13, - SIGALRM = 14, - SIGTERM = 15, - } - assert_gt( - pty.IPCSignal.size(), - signals.size() - 1, - "Expected Signal enum to have at least %d members." % signals.size() - ) - for signame in signals.keys(): - assert_has(pty.IPCSignal, signame, "Expected Signal enum to have member %s." % signame) - assert_eq( - pty.IPCSignal[signame], - signals[signame], - "Expected Signal enum member %s to have value %d." % [signame, signals[signame]] - ) + assert_eq(pty.IPCSIGNAL_SIGHUP, 1, "Expected pty to have IPCSIGNAL_SIGHUP.") + assert_eq(pty.IPCSIGNAL_SIGINT, 2, "Expected pty to have IPCSIGNAL_SIGINT.") + assert_eq(pty.IPCSIGNAL_SIGQUIT, 3, "Expected pty to have IPCSIGNAL_SIGQUIT.") + assert_eq(pty.IPCSIGNAL_SIGILL, 4, "Expected pty to have IPCSIGNAL_SIGILL.") + assert_eq(pty.IPCSIGNAL_SIGTRAP, 5, "Expected pty to have IPCSIGNAL_SIGTRAP.") + assert_eq(pty.IPCSIGNAL_SIGABRT, 6, "Expected pty to have IPCSIGNAL_SIGABRT.") + assert_eq(pty.IPCSIGNAL_SIGFPE, 8, "Expected pty to have IPCSIGNAL_SIGFPE.") + assert_eq(pty.IPCSIGNAL_SIGKILL, 9, "Expected pty to have IPCSIGNAL_SIGKILL.") + assert_eq(pty.IPCSIGNAL_SIGSEGV, 11, "Expected pty to have IPCSIGNAL_SIGSEGV.") + assert_eq(pty.IPCSIGNAL_SIGPIPE, 13, "Expected pty to have IPCSIGNAL_SIGPIPE.") + assert_eq(pty.IPCSIGNAL_SIGALRM, 14, "Expected pty to have IPCSIGNAL_SIGALRM.") + assert_eq(pty.IPCSIGNAL_SIGTERM, 15, "Expected pty to have IPCSIGNAL_SIGTERM.")