Rewrite terminal.cpp

Rewrites the Terminal class as a GDExtension to be used directly in
Godot without a terminal.gd proxy.

Breaks a lot of things in its current state (e.g. signals and other
functions have not be implemented yet), but does add support for
transparent colors and true color inversion. It also seems to
be about 4x faster (FPS-wise) than the old version with some basic
stress testing.

Old source code has been moved to a different directory to be copied
over and/or rewritten piece by piece.
This commit is contained in:
Leroy Hopson 2024-02-07 00:01:14 +13:00
parent 7d2e22530e
commit a849423096
No known key found for this signature in database
GPG key ID: D2747312A6DB51AA
29 changed files with 1431 additions and 857 deletions

View file

@ -1,66 +1,39 @@
#!/usr/bin/env python #!/usr/bin/env python
# SPDX-FileCopyrightText: 2020-2023 Leroy Hopson <godot-xterm@leroy.geek.nz> # SPDX-FileCopyrightText: 2020-2024 Leroy Hopson <godot-xterm@leroy.nix.nz>
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import os import os
import sys
env = SConscript("./thirdparty/godot-cpp/SConstruct") env = SConscript("thirdparty/godot-cpp/SConstruct")
env['ENV'] = os.environ env['ENV'] = os.environ
opts = Variables([], ARGUMENTS) VariantDir('build', 'src', duplicate=0)
opts.Add(BoolVariable( env['OBJPREFIX'] = os.path.join('build', '')
'disable_pty',
'Disables the PTY and its dependencies (LibuvUtils and Pipe). Has no effect on platforms where PTY is not supported',
False
))
opts.Update(env)
Help(opts.GenerateHelpText(env))
env.Append(CPPPATH=[ env.Append(CPPPATH=[
'src/', "thirdparty/libtsm/src/tsm",
'thirdparty/libtsm/src/tsm', "thirdparty/libtsm/external",
'thirdparty/libtsm/external', "thirdparty/libtsm/src/shared",
'thirdparty/libtsm/src/shared',
'thirdparty/libuv/include',
]) ])
sources = Glob('thirdparty/libtsm/src/tsm/*.c') sources = Glob("src/*.cpp") + Glob("thirdparty/libtsm/src/tsm/*.c")
sources.append([ sources.append([
'thirdparty/libtsm/external/wcwidth/wcwidth.c', 'thirdparty/libtsm/external/wcwidth/wcwidth.c',
'thirdparty/libtsm/src/shared/shl-htable.c', 'thirdparty/libtsm/src/shared/shl-htable.c',
'src/register_types.cpp',
'src/constants.cpp',
'src/terminal.cpp',
]) ])
if env['disable_pty'] or env['platform'] == 'javascript': if env["platform"] == "macos":
env.Append(CPPDEFINES=['_PTY_DISABLED']) library = env.SharedLibrary(
"bin/libgodot-xterm.{}.{}.framework/libgodot-xterm.{}.{}".format(
env["platform"], env["target"], env["platform"], env["target"]
),
source=sources,
)
else: else:
sources.append('src/pipe.cpp') library = env.SharedLibrary(
sources.append('src/libuv_utils.cpp') "bin/libgodot-xterm{}{}".format(env["suffix"], env["SHLIBSUFFIX"]),
if env['platform'] != 'windows': source=sources,
sources.append('src/node_pty/unix/pty.cc') )
env.Append(LIBS=['util', env.File('thirdparty/libuv/build/libuv_a.a')])
else:
sources.append('src/node_pty/win/conpty.cc')
env.Append(LIBS=[
env.File('thirdparty/libuv/build/{}/uv_a.lib'.format(env["target"].capitalize())),
'Advapi32.lib',
'Iphlpapi.lib',
'user32.lib',
'userenv.lib',
'Ws2_32.lib',
])
env.Append(LINKFLAGS=['-static-libstdc++'])
library = env.SharedLibrary(
target='bin/libgodot-xterm{}{}'.format(
env['suffix'],
env['SHLIBSUFFIX'],
), source=sources
)
Default(library) Default(library)

View file

@ -1,2 +0,0 @@
*
!.gitignore

View file

@ -0,0 +1,19 @@
[configuration]
entry_symbol = "godot_xterm_library_init"
compatibility_minimum = "4.2.1"
[libraries]
macos.debug = "res://addons/godot_xterm/native/bin/libgodot-xterm.macos.template_debug.framework"
macos.release = "res://addons/godot_xterm/native/bin/libgodot-xterm.macos.template_release.framework"
windows.debug.x86_32 = "res://addons/godot_xterm/native/bin/libgodot-xterm.windows.template_debug.x86_32.dll"
windows.release.x86_32 = "res://addons/godot_xterm/native/bin/libgodot-xterm.windows.template_release.x86_32.dll"
windows.debug.x86_64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_debug.x86_64.so"
linux.release.x86_64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_release.x86_64.so"
linux.debug.arm64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_debug.arm64.so"
linux.release.arm64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_release.arm64.so"
linux.debug.rv64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_debug.rv64.so"
linux.release.rv64 = "res://addons/godot_xterm/native/bin/libgodot-xterm.linux.template_release.rv64.so"

View file

@ -1,59 +1,59 @@
--- ---
services: services:
javascript: javascript:
build: build:
context: . context: .
dockerfile: javascript.Dockerfile dockerfile: javascript.Dockerfile
user: ${UID_GID} user: ${UID_GID}
volumes: volumes:
- .:/src - .:/src
command: command:
- /bin/bash - /bin/bash
- -c - -c
- | - |
cd /src/thirdparty/godot-cpp cd /src/thirdparty/godot-cpp
scons platform=javascript target=$${TARGET:-debug} -j$$(nproc) scons platform=javascript target=$${TARGET:-debug} -j$$(nproc)
cd /src cd /src
scons platform=javascript target=$${TARGET:-debug} -j$$(nproc) scons platform=javascript target=$${TARGET:-debug} -j$$(nproc)
libuv-linux: libuv-linux:
user: ${UID_GID} user: ${UID_GID}
build: build:
context: . context: .
dockerfile: linux.Dockerfile dockerfile: linux.Dockerfile
volumes: volumes:
- ./thirdparty/libuv:/libuv - ./thirdparty/libuv:/libuv
working_dir: /libuv working_dir: /libuv
command: command:
- /bin/bash - /bin/bash
- -c - -c
- | - |
target=$${TARGET:-release} target=$${TARGET:-release}
arch=$${ARCH:-x86_64} arch=$${ARCH:-x86_64}
mkdir build 2>/dev/null ; mkdir build 2>/dev/null ;
args="-DCMAKE_BUILD_TYPE=$$target \ args="-DCMAKE_BUILD_TYPE=$$target \
-DBUILD_SHARED_LIBS=OFF \ -DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=TRUE" -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE"
if [[ $$arch == "x86_32" ]]; then if [[ $$arch == "x86_32" ]]; then
args="$$args -DCMAKE_SYSTEM_PROCESSOR=i686 -DCMAKE_C_FLAGS=-m32"; args="$$args -DCMAKE_SYSTEM_PROCESSOR=i686 -DCMAKE_C_FLAGS=-m32";
else else
args="$$args -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_C_FLAGS="; args="$$args -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_C_FLAGS=";
fi fi
pushd build pushd build
cmake .. $$args cmake .. $$args
popd popd
cmake --build build cmake --build build
libgodot-xterm-linux: libgodot-xterm-linux:
user: ${UID_GID} user: ${UID_GID}
build: build:
context: . context: .
dockerfile: linux.Dockerfile dockerfile: linux.Dockerfile
volumes: volumes:
- .:/godot-xterm - .:/godot-xterm
working_dir: /godot-xterm working_dir: /godot-xterm
environment: environment:
- SCONS_CACHE=/scons-cache - SCONS_CACHE=/scons-cache
command: command:
- /bin/bash - /bin/bash
- -c - -c
- | - |
scons target=template_$${TARGET:-debug} arch=$${ARCH:-x86_64} scons target=template_$${TARGET:-debug} arch=$${ARCH:-x86_64}

View file

@ -2,17 +2,6 @@
#include "terminal.h" #include "terminal.h"
#if !defined(_PTY_DISABLED)
#include "libuv_utils.h"
#include "pipe.h"
#if defined(__linux__) || defined(__APPLE__)
#include "node_pty/unix/pty.h"
#endif
#if defined(__WIN32)
// #include "node_pty/win/conpty.h"
#endif
#endif
#include <gdextension_interface.h> #include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp> #include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp> #include <godot_cpp/godot.hpp>
@ -25,16 +14,6 @@ void initialize_godot_xterm_module(ModuleInitializationLevel p_level) {
} }
ClassDB::register_class<Terminal>(); ClassDB::register_class<Terminal>();
#if !defined(_PTY_DISABLED)
ClassDB::register_class<Pipe>();
ClassDB::register_class<LibuvUtils>();
#if defined(__linux__) || defined(__APPLE__)
ClassDB::register_class<PTYUnix>();
#endif
#if defined(__WIN32)
// ClassDB::register_class<ConPTY>();
#endif
#endif
} }
void uninitialize_godot_xterm_module(ModuleInitializationLevel p_level) { void uninitialize_godot_xterm_module(ModuleInitializationLevel p_level) {
@ -44,7 +23,7 @@ void uninitialize_godot_xterm_module(ModuleInitializationLevel p_level) {
} }
extern "C" { extern "C" {
// Initialization // Initialization.
GDExtensionBool GDE_EXPORT GDExtensionBool GDE_EXPORT
godot_xterm_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, godot_xterm_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address,
const GDExtensionClassLibraryPtr p_library, const GDExtensionClassLibraryPtr p_library,

File diff suppressed because it is too large Load diff

View file

@ -1,164 +1,110 @@
// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson <godot-xterm@leroy.geek.nz> // SPDX-FileCopyrightText: 2021-2024 Leroy Hopson <godot-xterm@leroy.nix.nz>
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#ifndef GODOT_XTERM_TERMINAL_H #pragma once
#define GODOT_XTERM_TERMINAL_H
#include <godot_cpp/classes/control.hpp> #include <godot_cpp/classes/control.hpp>
#include <godot_cpp/classes/font.hpp> #include <godot_cpp/classes/image_texture.hpp>
#include <godot_cpp/classes/input_event.hpp> #include <godot_cpp/classes/rendering_server.hpp>
#include <godot_cpp/classes/input_event_key.hpp> #include <godot_cpp/classes/shader_material.hpp>
#include <godot_cpp/classes/input_event_mouse.hpp>
#include <godot_cpp/classes/input_event_mouse_button.hpp>
#include <godot_cpp/classes/sub_viewport.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/timer.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>
#include <libtsm.h> #include <libtsm.h>
#include <map>
#include <vector>
using namespace godot; namespace godot
{
class Terminal : public Control { class Terminal : public Control
GDCLASS(Terminal, Control) {
GDCLASS(Terminal, Control)
public: private:
Terminal(); static constexpr const char *COLOR_NAMES[] = {
~Terminal(); "ansi_0_color", "ansi_1_color", "ansi_2_color", "ansi_3_color", "ansi_4_color", "ansi_5_color", "ansi_6_color", "ansi_7_color",
"ansi_8_color", "ansi_9_color", "ansi_10_color", "ansi_11_color", "ansi_12_color", "ansi_13_color", "ansi_14_color", "ansi_15_color",
"foreground_color", "background_color",
};
enum UpdateMode { static constexpr const char *FONT_TYPES[] = {
DISABLED, "normal_font", "bold_font", "italics_font", "bold_italics_font",
AUTO, };
ALL,
ALL_NEXT_FRAME, public:
enum AttrFlags
{
INVERSE = 1 << 0,
BLINK = 1 << 1,
};
Terminal();
~Terminal();
void set_cols(const int p_cols);
int get_cols() const;
void set_rows(const int p_rows);
int get_rows() const;
void set_max_scrollback(const int p_max_scrollback);
int get_max_scrollback() const;
void write(Variant data);
protected:
static void _bind_methods();
private:
unsigned int max_scrollback;
unsigned int cols;
unsigned int rows;
RenderingServer *rs;
tsm_screen *screen;
tsm_vte *vte;
tsm_age_t framebuffer_age;
static void _write_cb(struct tsm_vte *vte, const char *u8, size_t len,
void *data);
static int _draw_cb(struct tsm_screen *con, uint64_t id, const uint32_t *ch,
size_t len, unsigned int width, unsigned int posx,
unsigned int posy, const struct tsm_screen_attr *attr,
tsm_age_t age, void *data);
PackedColorArray palette;
Ref<Font> font;
int32_t font_size;
Vector2 size;
Vector2 cell_size;
Ref<Image> attr_image;
Ref<ImageTexture> attr_texture;
// Background.
Ref<Image> back_image;
Ref<ImageTexture> back_texture;
Ref<Shader> back_shader;
Ref<ShaderMaterial> back_material;
RID back_canvas_item;
// Foreground.
RID char_shader, char_material, char_canvas_item, canvas, viewport,
fore_canvas_item;
Ref<Shader> fore_shader;
Ref<ShaderMaterial> fore_material;
void _notification(const int what);
void initialize_rendering();
void update_theme();
void update_sizes(bool force = false);
void draw_screen();
void refresh();
void cleanup_rendering();
bool _set(const StringName &p_name, const Variant &p_value);
void _get_property_list(List<PropertyInfo> *p_list) const;
bool _is_valid_color_name(const String &p_name);
bool _is_valid_font_type(const String &p_name);
}; };
static const UpdateMode UPDATE_MODE_DISABLED = UpdateMode::DISABLED; } // namespace godot
static const UpdateMode UPDATE_MODE_AUTO = UpdateMode::AUTO;
static const UpdateMode UPDATE_MODE_ALL = UpdateMode::ALL;
static const UpdateMode UPDATE_MODE_ALL_NEXT_FRAME =
UpdateMode::ALL_NEXT_FRAME;
bool copy_on_selection = false;
void set_copy_on_selection(bool value);
bool get_copy_on_selection();
UpdateMode update_mode = UPDATE_MODE_AUTO;
void set_update_mode(UpdateMode value);
UpdateMode get_update_mode();
double bell_cooldown = 0.1f;
void set_bell_cooldown(double value);
double get_bell_cooldown();
bool bell_muted = false;
void set_bell_muted(bool value);
bool get_bell_muted();
bool blink_enabled = true;
void set_blink_enabled(bool value);
bool get_blink_enabled();
double blink_time_off = 0.3;
void set_blink_time_off(double value);
double get_blink_time_off();
double blink_time_on = 0.6;
void set_blink_time_on(double value);
double get_blink_time_on();
void clear();
String copy_all();
String copy_selection();
int get_cols();
int get_rows();
void write(Variant data);
void _gui_input(Ref<InputEvent> event);
void _notification(int what);
void _flush();
void _on_back_buffer_draw();
void _on_selection_held();
void _toggle_blink();
protected:
static void _bind_methods();
private:
struct ColorDef {
const char *default_color;
tsm_vte_color tsm_color;
};
struct ThemeCache {
int font_size = 0;
std::map<String, Ref<Font>> fonts = std::map<String, Ref<Font>>{};
std::map<String, Color> colors = std::map<String, Color>{};
} theme_cache;
typedef std::map<const char *, ColorDef> ColorMap;
typedef std::pair<Color, Color> ColorPair;
typedef std::map<const char *, const char *> FontMap;
typedef std::map<std::pair<Key, char32_t>, uint32_t> KeyMap;
static const KeyMap KEY_MAP;
static const ColorMap COLORS;
static const FontMap FONTS;
enum SelectionMode { NONE, POINTER };
Control *back_buffer = new Control();
SubViewport *sub_viewport = new SubViewport();
TextureRect *front_buffer = new TextureRect();
Timer *bell_timer = new Timer();
Timer *blink_timer = new Timer();
Timer *selection_timer = new Timer();
tsm_screen *screen;
tsm_vte *vte;
Array write_buffer = Array();
int cols = 80;
int rows = 24;
// Whether blinking characters are visible. Not whether blinking is enabled
// which is determined by `blink_enabled`.
bool blink_on = true;
Vector2 cell_size = Vector2(0, 0);
std::map<int, Color> palette = {};
tsm_age_t framebuffer_age;
Ref<InputEventKey> last_input_event_key;
bool selecting = false;
SelectionMode selection_mode = SelectionMode::NONE;
static void _bell_cb(tsm_vte *vte, void *data);
static int _text_draw_cb(tsm_screen *con, uint64_t id, const uint32_t *ch,
size_t len, unsigned int width, unsigned int col,
unsigned int row, const tsm_screen_attr *attr,
tsm_age_t age, void *data);
static void _write_cb(tsm_vte *vte, const char *u8, size_t len, void *data);
void _draw_background(int row, int col, Color bgcol, int width);
void _draw_foreground(int row, int col, char *ch, const tsm_screen_attr *attr,
Color fgcol);
ColorPair _get_cell_colors(const tsm_screen_attr *attr);
void _handle_key_input(Ref<InputEventKey> event);
void _handle_mouse_wheel(Ref<InputEventMouseButton> event);
void _handle_selection(Ref<InputEventMouse> event);
void _recalculate_size();
void _refresh();
void _update_theme_item_cache();
};
VARIANT_ENUM_CAST(Terminal::UpdateMode);
#endif // GODOT_XTERM_TERMINAL_H

View file

@ -0,0 +1,654 @@
// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson <godot-xterm@leroy.geek.nz>
// SPDX-License-Identifier: MIT
#include "terminal.h"
#include <algorithm>
#include <godot_cpp/classes/display_server.hpp>
#include <godot_cpp/classes/input.hpp>
#include <godot_cpp/classes/input_event_mouse_motion.hpp>
#include <godot_cpp/classes/rendering_server.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/classes/theme.hpp>
#include <godot_cpp/classes/theme_db.hpp>
#include <godot_cpp/classes/viewport_texture.hpp>
#include <godot_cpp/core/object.hpp>
#include <godot_cpp/variant/color.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <string>
#include <xkbcommon/xkbcommon-keysyms.h>
#define UNICODE_MAX 0x10FFFF
using namespace godot;
Terminal::Terminal() {
// Ensure we write to terminal before the frame is drawn. Otherwise, the
// terminal state may be updated but not drawn until it is updated again,
// which may not happen for some time.
RenderingServer::get_singleton()->connect("frame_pre_draw",
Callable(this, "_flush"));
// Override default focus mode.
set_focus_mode(FOCUS_ALL);
// Name our nodes for easier debugging.
back_buffer->set_name("BackBuffer");
sub_viewport->set_name("SubViewport");
front_buffer->set_name("FrontBuffer");
// Ensure buffers always have correct size.
back_buffer->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
front_buffer->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
// Setup back buffer.
back_buffer->connect("draw", Callable(this, "_on_back_buffer_draw"));
// Setup sub viewport.
sub_viewport->set_handle_input_locally(false);
sub_viewport->set_transparent_background(true);
sub_viewport->set_snap_controls_to_pixels(false);
sub_viewport->set_update_mode(SubViewport::UPDATE_WHEN_PARENT_VISIBLE);
sub_viewport->set_clear_mode(SubViewport::CLEAR_MODE_NEVER);
sub_viewport->add_child(back_buffer);
add_child(sub_viewport);
// Setup bell timer.
bell_timer->set_name("BellTimer");
bell_timer->set_one_shot(true);
add_child(bell_timer);
// Setup blink timer.
blink_timer->set_name("BlinkTimer");
blink_timer->set_one_shot(true);
blink_timer->connect("timeout", Callable(this, "_toggle_blink"));
add_child(blink_timer);
// Setup selection timer.
selection_timer->set_name("SelectionTimer");
selection_timer->set_wait_time(0.05);
selection_timer->connect("timeout", Callable(this, "_on_selection_held"));
add_child(selection_timer);
// Setup front buffer.
front_buffer->set_texture(sub_viewport->get_texture());
add_child(front_buffer);
framebuffer_age = 0;
update_mode = UpdateMode::AUTO;
if (tsm_screen_new(&screen, NULL, NULL)) {
ERR_PRINT("Error creating new tsm screen.");
}
tsm_screen_set_max_sb(screen, 1000);
if (tsm_vte_new(&vte, screen, &Terminal::_write_cb, this, NULL, NULL)) {
ERR_PRINT("Error creating new tsm vte.");
}
tsm_vte_set_bell_cb(vte, &Terminal::_bell_cb, this);
_update_theme_item_cache();
}
Terminal::~Terminal() {
back_buffer->queue_free();
sub_viewport->queue_free();
front_buffer->queue_free();
}
void Terminal::set_copy_on_selection(bool value) { copy_on_selection = value; }
bool Terminal::get_copy_on_selection() { return copy_on_selection; }
void Terminal::set_update_mode(Terminal::UpdateMode value) {
update_mode = value;
};
Terminal::UpdateMode Terminal::get_update_mode() { return update_mode; }
void Terminal::set_bell_cooldown(double value) { bell_cooldown = value; }
double Terminal::get_bell_cooldown() { return bell_cooldown; }
void Terminal::set_bell_muted(bool value) { bell_muted = value; }
bool Terminal::get_bell_muted() { return bell_muted; }
void Terminal::set_blink_enabled(bool value) {
blink_enabled = value;
if (!blink_enabled)
blink_timer->stop();
_refresh();
}
bool Terminal::get_blink_enabled() { return blink_enabled; }
void Terminal::set_blink_time_off(double value) {
blink_time_off = value;
if (!blink_on && !blink_timer->is_stopped()) {
double time_left = blink_timer->get_time_left();
blink_timer->start(std::min(blink_time_off, time_left));
}
}
double Terminal::get_blink_time_off() { return blink_time_off; }
void Terminal::set_blink_time_on(double value) {
blink_time_on = value;
if (blink_on && !blink_timer->is_stopped()) {
double time_left = blink_timer->get_time_left();
blink_timer->start(std::min(blink_time_on, time_left));
}
}
double Terminal::get_blink_time_on() { return blink_time_on; }
void Terminal::clear() {
Vector2 initial_size = get_size();
set_size(Vector2(initial_size.x, cell_size.y));
tsm_screen_clear_sb(screen);
set_size(initial_size);
back_buffer->queue_redraw();
}
String Terminal::copy_all() {
char *out = nullptr;
int len = tsm_screen_copy_all(screen, &out);
String result = String(out);
std::free(out);
return result;
}
String Terminal::copy_selection() {
char *out = nullptr;
int len = tsm_screen_selection_copy(screen, &out);
String result = String(out);
std::free(out);
return result;
}
int Terminal::get_cols() { return cols; }
int Terminal::get_rows() { return rows; }
void Terminal::write(Variant data) {
switch (data.get_type()) {
case Variant::PACKED_BYTE_ARRAY:
break;
case Variant::STRING:
data = ((String)data).to_utf8_buffer();
break;
default:
ERR_PRINT("Expected data to be a String or PackedByteArray.");
}
write_buffer.push_back(data);
queue_redraw();
}
void Terminal::_gui_input(Ref<InputEvent> event) {
_handle_key_input(event);
_handle_selection(event);
_handle_mouse_wheel(event);
}
void Terminal::_notification(int what) {
switch (what) {
case NOTIFICATION_RESIZED:
_recalculate_size();
sub_viewport->set_size(get_size());
_refresh();
break;
case NOTIFICATION_THEME_CHANGED:
_update_theme_item_cache();
_refresh();
break;
}
}
void Terminal::_flush() {
if (write_buffer.is_empty())
return;
for (int i = 0; i < write_buffer.size(); i++) {
PackedByteArray data = static_cast<PackedByteArray>(write_buffer[i]);
tsm_vte_input(vte, (char *)data.ptr(), data.size());
}
write_buffer.clear();
back_buffer->queue_redraw();
}
void Terminal::_on_back_buffer_draw() {
if (update_mode == UpdateMode::DISABLED) {
return;
}
if ((update_mode > UpdateMode::AUTO) || framebuffer_age == 0) {
Color background_color = palette[TSM_COLOR_BACKGROUND];
back_buffer->draw_rect(back_buffer->get_rect(), background_color);
}
int prev_framebuffer_age = framebuffer_age;
framebuffer_age = tsm_screen_draw(screen, &Terminal::_text_draw_cb, this);
if (update_mode == UpdateMode::ALL_NEXT_FRAME && prev_framebuffer_age != 0)
update_mode = UpdateMode::AUTO;
}
void Terminal::_on_selection_held() {
if (!(Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) ||
selection_mode == SelectionMode::NONE) {
if (copy_on_selection)
DisplayServer::get_singleton()->clipboard_set_primary(copy_selection());
selection_timer->stop();
return;
}
Vector2 target = get_local_mouse_position() / cell_size;
tsm_screen_selection_target(screen, target.x, target.y);
back_buffer->queue_redraw();
selection_timer->start();
}
void Terminal::_toggle_blink() {
if (blink_enabled) {
blink_on = !blink_on;
_refresh();
}
}
void Terminal::_bind_methods() {
// Properties.
ClassDB::bind_method(D_METHOD("set_copy_on_selection", "value"),
&Terminal::set_copy_on_selection);
ClassDB::bind_method(D_METHOD("get_copy_on_selection"),
&Terminal::get_copy_on_selection);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "copy_on_selection"),
"set_copy_on_selection", "get_copy_on_selection");
ClassDB::bind_method(D_METHOD("set_update_mode", "value"),
&Terminal::set_update_mode);
ClassDB::bind_method(D_METHOD("get_update_mode"), &Terminal::get_update_mode);
ADD_PROPERTY(PropertyInfo(Variant::INT, "update_mode", PROPERTY_HINT_ENUM,
"Disabled,Auto,All,All Next Frame"),
"set_update_mode", "get_update_mode");
ADD_GROUP("Bell", "bell_");
ClassDB::bind_method(D_METHOD("set_bell_cooldown", "value"),
&Terminal::set_bell_cooldown);
ClassDB::bind_method(D_METHOD("get_bell_cooldown"),
&Terminal::get_bell_cooldown);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bell_cooldown"),
"set_bell_cooldown", "get_bell_cooldown");
ClassDB::bind_method(D_METHOD("set_bell_muted", "value"),
&Terminal::set_bell_muted);
ClassDB::bind_method(D_METHOD("get_bell_muted"), &Terminal::get_bell_muted);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "bell_muted"), "set_bell_muted",
"get_bell_muted");
ADD_GROUP("Blink", "blink_");
ClassDB::bind_method(D_METHOD("set_blink_enabled", "value"),
&Terminal::set_blink_enabled);
ClassDB::bind_method(D_METHOD("get_blink_enabled"),
&Terminal::get_blink_enabled);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "blink_enabled"),
"set_blink_enabled", "get_blink_enabled");
ClassDB::bind_method(D_METHOD("get_blink_time_off"),
&Terminal::get_blink_time_off);
ClassDB::bind_method(D_METHOD("set_blink_time_off", "value"),
&Terminal::set_blink_time_off);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "blink_time_off"),
"set_blink_time_off", "get_blink_time_off");
ClassDB::bind_method(D_METHOD("set_blink_time_on", "value"),
&Terminal::set_blink_time_on);
ClassDB::bind_method(D_METHOD("get_blink_time_on"),
&Terminal::get_blink_time_on);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "blink_time_on"),
"set_blink_time_on", "get_blink_time_on");
// Methods.
ClassDB::bind_method(D_METHOD("clear"), &Terminal::clear);
ClassDB::bind_method(D_METHOD("copy_all"), &Terminal::copy_all);
ClassDB::bind_method(D_METHOD("copy_selection"), &Terminal::copy_selection);
ClassDB::bind_method(D_METHOD("get_cols"), &Terminal::get_cols);
ClassDB::bind_method(D_METHOD("get_rows"), &Terminal::get_rows);
ClassDB::bind_method(D_METHOD("write", "data"), &Terminal::write);
// Signals.
ADD_SIGNAL(MethodInfo("bell"));
ADD_SIGNAL(MethodInfo("data_sent",
PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("key_pressed",
PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"),
PropertyInfo(Variant::OBJECT, "event")));
ADD_SIGNAL(
MethodInfo("size_changed", PropertyInfo(Variant::VECTOR2, "new_size")));
// Enumerations.
BIND_ENUM_CONSTANT(UPDATE_MODE_DISABLED);
BIND_ENUM_CONSTANT(UPDATE_MODE_AUTO);
BIND_ENUM_CONSTANT(UPDATE_MODE_ALL);
BIND_ENUM_CONSTANT(UPDATE_MODE_ALL_NEXT_FRAME);
// Private methods (must be exposed as they are connected to signals).
ClassDB::bind_method(D_METHOD("_flush"), &Terminal::_flush);
ClassDB::bind_method(D_METHOD("_on_back_buffer_draw"),
&Terminal::_on_back_buffer_draw);
ClassDB::bind_method(D_METHOD("_on_selection_held"),
&Terminal::_on_selection_held);
ClassDB::bind_method(D_METHOD("_toggle_blink"), &Terminal::_toggle_blink);
}
void Terminal::_bell_cb(tsm_vte *vte, void *data) {
Terminal *term = static_cast<Terminal *>(data);
if (!term->bell_muted && term->bell_cooldown == 0 ||
term->bell_timer->get_time_left() == 0) {
term->emit_signal("bell");
if (term->bell_cooldown > 0)
term->bell_timer->start(term->bell_cooldown);
}
}
int Terminal::_text_draw_cb(tsm_screen *con, uint64_t id, const uint32_t *ch,
size_t len, unsigned int width, unsigned int col,
unsigned int row, const tsm_screen_attr *attr,
tsm_age_t age, void *data) {
Terminal *term = static_cast<Terminal *>(data);
if (term->update_mode == Terminal::UpdateMode::AUTO && age != 0 &&
age <= term->framebuffer_age) {
return 0;
}
if (width < 1) { // No foreground or background to draw.
return 0;
}
ColorPair color_pair = term->_get_cell_colors(attr);
term->_draw_background(row, col, color_pair.first, width);
if (len < 1) // No foreground to draw.
return 0;
size_t ulen = 0;
char buf[5] = {0};
char *utf8 = tsm_ucs4_to_utf8_alloc(ch, len, &ulen);
memcpy(buf, utf8, ulen);
term->_draw_foreground(row, col, buf, attr, color_pair.second);
return 0;
}
void Terminal::_write_cb(tsm_vte *vte, const char *u8, size_t len, void *data) {
Terminal *term = static_cast<Terminal *>(data);
PackedByteArray bytes;
bytes.resize(len);
{ memcpy(bytes.ptrw(), u8, len); }
if (len > 0) {
if (term->last_input_event_key.is_valid()) {
// The callback was fired from a key press event so emit the "key_pressed"
// signal.
term->emit_signal("key_pressed", bytes.get_string_from_utf8(),
term->last_input_event_key);
term->last_input_event_key.unref();
}
term->emit_signal("data_sent", bytes);
}
}
void Terminal::_draw_background(int row, int col, Color bgcolor,
int width = 1) {
/* Draw the background */
Vector2 background_pos = Vector2(col * cell_size.x, row * cell_size.y);
Rect2 background_rect = Rect2(background_pos, cell_size * Vector2(width, 1));
back_buffer->draw_rect(background_rect, bgcolor);
}
void Terminal::_draw_foreground(int row, int col, char *ch,
const tsm_screen_attr *attr, Color fgcolor) {
Ref<Font> font;
if (attr->bold && attr->italic) {
font = theme_cache.fonts["bold_italics_font"];
} else if (attr->bold) {
font = theme_cache.fonts["bold_font"];
} else if (attr->italic) {
font = theme_cache.fonts["italics_font"];
} else {
font = theme_cache.fonts["normal_font"];
}
if (attr->blink && blink_enabled) {
if (blink_timer->is_stopped())
blink_timer->start(blink_on ? blink_time_on : blink_time_off);
if (!blink_on)
return;
}
int font_height = font->get_height(theme_cache.font_size);
Vector2 foreground_pos =
Vector2(col * cell_size.x, row * cell_size.y + font_height / 1.25);
back_buffer->draw_string(font, foreground_pos, ch, HORIZONTAL_ALIGNMENT_LEFT,
-1, theme_cache.font_size, fgcolor);
if (attr->underline)
back_buffer->draw_string(font, foreground_pos, "_",
HORIZONTAL_ALIGNMENT_LEFT, -1,
theme_cache.font_size, fgcolor);
}
Terminal::ColorPair Terminal::_get_cell_colors(const tsm_screen_attr *attr) {
Color fgcol, bgcol;
int8_t fccode = attr->fccode;
int8_t bccode = attr->bccode;
// Get foreground color.
if (fccode && palette.count(fccode)) {
fgcol = palette[fccode];
} else {
fgcol = Color(attr->fr / 255.0f, attr->fg / 255.0f, attr->fb / 255.0f);
if (fccode != -1)
palette.insert({fccode, fgcol});
}
// Get background color.
if (bccode && palette.count(bccode)) {
bgcol = palette[bccode];
} else {
bgcol = Color(attr->br / 255.0f, attr->bg / 255.0f, attr->bb / 255.0f);
if (bccode != -1)
palette.insert({bccode, bgcol});
}
if (attr->inverse)
std::swap(bgcol, fgcol);
return std::make_pair(bgcol, fgcol);
}
void Terminal::_handle_key_input(Ref<InputEventKey> event) {
if (!event.is_valid() || !event->is_pressed())
return;
const Key keycode = event->get_keycode();
char32_t unicode = event->get_unicode();
uint32_t ascii = unicode <= 127 ? unicode : 0;
unsigned int mods = 0;
if (event->is_alt_pressed())
mods |= TSM_ALT_MASK;
if (event->is_ctrl_pressed())
mods |= TSM_CONTROL_MASK;
if (event->is_shift_pressed())
mods |= TSM_SHIFT_MASK;
std::pair<Key, char32_t> key = {keycode, unicode};
uint32_t keysym =
(KEY_MAP.count(key) > 0) ? KEY_MAP.at(key) : XKB_KEY_NoSymbol;
last_input_event_key = event;
tsm_vte_handle_keyboard(vte, keysym, ascii, mods,
unicode ? unicode : TSM_VTE_INVALID);
// Return to the bottom of the scrollback buffer if we scrolled up. Ignore
// modifier keys pressed in isolation or if Ctrl+Shift modifier keys are
// pressed.
std::set<Key> mod_keys = {KEY_ALT, KEY_SHIFT, KEY_CTRL, KEY_META};
if (mod_keys.find(keycode) == mod_keys.end() &&
!(event->is_ctrl_pressed() && event->is_shift_pressed())) {
tsm_screen_sb_reset(screen);
back_buffer->queue_redraw();
}
// Prevent focus changing to other inputs when pressing Tab or Arrow keys.
std::set<Key> 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<InputEventMouseButton> event) {
if (!event.is_valid() || !event->is_pressed())
return;
void (*scroll_func)(tsm_screen *, unsigned int) = nullptr;
switch (event->get_button_index()) {
case MOUSE_BUTTON_WHEEL_UP:
scroll_func = &tsm_screen_sb_up;
break;
case MOUSE_BUTTON_WHEEL_DOWN:
scroll_func = &tsm_screen_sb_down;
break;
};
if (scroll_func != nullptr) {
// Scroll 5 times as fast as normal if alt is pressed (like TextEdit).
// Otherwise, just scroll 3 lines.
int speed = event->is_alt_pressed() ? 15 : 3;
double factor = event->get_factor();
(*scroll_func)(screen, speed * factor);
back_buffer->queue_redraw();
}
}
void Terminal::_handle_selection(Ref<InputEventMouse> event) {
if (!event.is_valid())
return;
Ref<InputEventMouseButton> mb = event;
if (mb.is_valid()) {
if (!mb->is_pressed() || !mb->get_button_index() == MOUSE_BUTTON_LEFT)
return;
if (selecting) {
selecting = false;
selection_mode = SelectionMode::NONE;
tsm_screen_selection_reset(screen);
back_buffer->queue_redraw();
}
selecting = false;
selection_mode = SelectionMode::POINTER;
return;
}
Ref<InputEventMouseMotion> mm = event;
if (mm.is_valid()) {
if ((mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT) &&
selection_mode != SelectionMode::NONE && !selecting) {
selecting = true;
Vector2 start = event->get_position() / cell_size;
tsm_screen_selection_start(screen, start.x, start.y);
back_buffer->queue_redraw();
selection_timer->start();
}
return;
}
}
void Terminal::_recalculate_size() {
Vector2 size = get_size();
cell_size = theme_cache.fonts["normal_font"]->get_string_size(
"W", HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size);
rows = std::max(1, (int)floor(size.y / cell_size.y));
cols = std::max(1, (int)floor(size.x / cell_size.x));
tsm_screen_resize(screen, cols, rows);
sub_viewport->set_size(size);
emit_signal("size_changed", Vector2(cols, rows));
}
void Terminal::_refresh() {
back_buffer->queue_redraw();
front_buffer->queue_redraw();
if (update_mode == UpdateMode::AUTO)
update_mode = UpdateMode::ALL_NEXT_FRAME;
}
void Terminal::_update_theme_item_cache() {
// Fonts.
for (std::map<const char *, const char *>::const_iterator iter =
Terminal::FONTS.begin();
iter != Terminal::FONTS.end(); ++iter) {
String name = iter->first;
Ref<Font> font = has_theme_font_override(name) ? get_theme_font(name)
: has_theme_font(name, "Terminal")
? get_theme_font(name, "Terminal")
: ThemeDB::get_singleton()->get_fallback_font();
theme_cache.fonts[name] = font;
}
// Font size.
theme_cache.font_size =
has_theme_font_size_override("font_size")
? get_theme_font_size("font_size")
: has_theme_font_size("font_size", "Terminal")
? get_theme_font_size("font_size", "Terminal")
: ThemeDB::get_singleton()->get_fallback_font_size();
// Colors.
uint8_t custom_palette[TSM_COLOR_NUM][3];
for (ColorMap::const_iterator iter = Terminal::COLORS.begin();
iter != Terminal::COLORS.end(); ++iter) {
String name = iter->first;
Color color = has_theme_color_override(name) ? get_theme_color(name)
: has_theme_color(name, "Terminal")
? get_theme_color(name, "Terminal")
: color = Color::html(iter->second.default_color);
theme_cache.colors[name] = color;
palette[iter->second.tsm_color] = color;
custom_palette[iter->second.tsm_color][0] = color.get_r8();
custom_palette[iter->second.tsm_color][1] = color.get_g8();
custom_palette[iter->second.tsm_color][2] = color.get_b8();
}
if (tsm_vte_set_custom_palette(vte, custom_palette))
ERR_PRINT("Error setting custom palette.");
if (tsm_vte_set_palette(vte, "custom"))
ERR_PRINT("Error setting palette to custom palette.");
_recalculate_size();
}

View file

@ -0,0 +1,164 @@
// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson <godot-xterm@leroy.geek.nz>
// SPDX-License-Identifier: MIT
#ifndef GODOT_XTERM_TERMINAL_H
#define GODOT_XTERM_TERMINAL_H
#include <godot_cpp/classes/control.hpp>
#include <godot_cpp/classes/font.hpp>
#include <godot_cpp/classes/input_event.hpp>
#include <godot_cpp/classes/input_event_key.hpp>
#include <godot_cpp/classes/input_event_mouse.hpp>
#include <godot_cpp/classes/input_event_mouse_button.hpp>
#include <godot_cpp/classes/sub_viewport.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/timer.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>
#include <libtsm.h>
#include <map>
#include <vector>
using namespace godot;
class Terminal : public Control {
GDCLASS(Terminal, Control)
public:
Terminal();
~Terminal();
enum UpdateMode {
DISABLED,
AUTO,
ALL,
ALL_NEXT_FRAME,
};
static const UpdateMode UPDATE_MODE_DISABLED = UpdateMode::DISABLED;
static const UpdateMode UPDATE_MODE_AUTO = UpdateMode::AUTO;
static const UpdateMode UPDATE_MODE_ALL = UpdateMode::ALL;
static const UpdateMode UPDATE_MODE_ALL_NEXT_FRAME =
UpdateMode::ALL_NEXT_FRAME;
bool copy_on_selection = false;
void set_copy_on_selection(bool value);
bool get_copy_on_selection();
UpdateMode update_mode = UPDATE_MODE_AUTO;
void set_update_mode(UpdateMode value);
UpdateMode get_update_mode();
double bell_cooldown = 0.1f;
void set_bell_cooldown(double value);
double get_bell_cooldown();
bool bell_muted = false;
void set_bell_muted(bool value);
bool get_bell_muted();
bool blink_enabled = true;
void set_blink_enabled(bool value);
bool get_blink_enabled();
double blink_time_off = 0.3;
void set_blink_time_off(double value);
double get_blink_time_off();
double blink_time_on = 0.6;
void set_blink_time_on(double value);
double get_blink_time_on();
void clear();
String copy_all();
String copy_selection();
int get_cols();
int get_rows();
void write(Variant data);
void _gui_input(Ref<InputEvent> event);
void _notification(int what);
void _flush();
void _on_back_buffer_draw();
void _on_selection_held();
void _toggle_blink();
protected:
static void _bind_methods();
private:
struct ColorDef {
const char *default_color;
tsm_vte_color tsm_color;
};
struct ThemeCache {
int font_size = 0;
std::map<String, Ref<Font>> fonts = std::map<String, Ref<Font>>{};
std::map<String, Color> colors = std::map<String, Color>{};
} theme_cache;
typedef std::map<const char *, ColorDef> ColorMap;
typedef std::pair<Color, Color> ColorPair;
typedef std::map<const char *, const char *> FontMap;
typedef std::map<std::pair<Key, char32_t>, uint32_t> KeyMap;
static const KeyMap KEY_MAP;
static const ColorMap COLORS;
static const FontMap FONTS;
enum SelectionMode { NONE, POINTER };
Control *back_buffer = new Control();
SubViewport *sub_viewport = new SubViewport();
TextureRect *front_buffer = new TextureRect();
Timer *bell_timer = new Timer();
Timer *blink_timer = new Timer();
Timer *selection_timer = new Timer();
tsm_screen *screen;
tsm_vte *vte;
Array write_buffer = Array();
int cols = 80;
int rows = 24;
// Whether blinking characters are visible. Not whether blinking is enabled
// which is determined by `blink_enabled`.
bool blink_on = true;
Vector2 cell_size = Vector2(0, 0);
std::map<int, Color> palette = {};
tsm_age_t framebuffer_age;
Ref<InputEventKey> last_input_event_key;
bool selecting = false;
SelectionMode selection_mode = SelectionMode::NONE;
static void _bell_cb(tsm_vte *vte, void *data);
static int _text_draw_cb(tsm_screen *con, uint64_t id, const uint32_t *ch,
size_t len, unsigned int width, unsigned int col,
unsigned int row, const tsm_screen_attr *attr,
tsm_age_t age, void *data);
static void _write_cb(tsm_vte *vte, const char *u8, size_t len, void *data);
void _draw_background(int row, int col, Color bgcol, int width);
void _draw_foreground(int row, int col, char *ch, const tsm_screen_attr *attr,
Color fgcol);
ColorPair _get_cell_colors(const tsm_screen_attr *attr);
void _handle_key_input(Ref<InputEventKey> event);
void _handle_mouse_wheel(Ref<InputEventMouseButton> event);
void _handle_selection(Ref<InputEventMouse> event);
void _recalculate_size();
void _refresh();
void _update_theme_item_cache();
};
VARIANT_ENUM_CAST(Terminal::UpdateMode);
#endif // GODOT_XTERM_TERMINAL_H

View file

@ -0,0 +1,38 @@
shader_type canvas_item;
uniform int cols;
uniform int rows;
uniform vec2 size; // Total size of the Terminal control.
uniform vec2 cell_size;
uniform vec4 background;
uniform sampler2D cell_colors; // Texture containing the cell colors, one pixel per cell.
const int INVERSE_FLAG = 1 << 0;
// Texture containing the cell attributes encoded using bit flags, one flag per cell in the R channel.
uniform sampler2D attributes;
void fragment() {
// Grid means the part of the terminal that contains the cells.
// Will vary depending on font size and the size of the control.
vec2 grid_size = vec2(cell_size.x * float(cols), cell_size.y * float(rows));
if (UV.x * size.x < grid_size.x && UV.y * size.y < grid_size.y) {
vec2 grid_uv = UV * size / cell_size;
int cell_x = int(grid_uv.x);
int cell_y = int(grid_uv.y);
vec2 sample_uv = (vec2(float(cell_x), float(cell_y)) + 0.5) / vec2(float(cols), float(rows));
vec4 color = texture(cell_colors, sample_uv);
int attr_flags = int(texture(attributes, sample_uv).r * 255.0 + 0.5);
if ((attr_flags & INVERSE_FLAG) != 0) {
color = vec4(1.0 - color.rgb, color.a);
}
COLOR = color;
} else {
// If outside the grid area, use the background color.
COLOR = background;
}
}

View file

@ -0,0 +1,7 @@
shader_type canvas_item;
void fragment() {
COLOR = texture(TEXTURE, UV);
// TODO: Check blink attribute and hide/show using the below.
//COLOR = vec4(0);
}