2021-07-08 15:10:34 +02:00
|
|
|
# Copyright (c) 2021, Leroy Hopson (MIT License).
|
|
|
|
#
|
|
|
|
# This file contains snippets of code derived from Godot's TextEdit node.
|
|
|
|
# These snippets are copyright of their authors and released under the MIT license:
|
|
|
|
# - Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur (MIT License).
|
|
|
|
# - Copyright (c) 2014-2021 Godot Engine contributors (MIT License).
|
2021-06-19 15:57:05 +02:00
|
|
|
tool
|
|
|
|
extends Control
|
|
|
|
|
|
|
|
signal data_sent(data)
|
|
|
|
signal key_pressed(data, event)
|
|
|
|
signal size_changed(new_size)
|
2021-07-03 16:57:19 +02:00
|
|
|
signal bell
|
2021-06-19 15:57:05 +02:00
|
|
|
|
|
|
|
enum UpdateMode {
|
|
|
|
DISABLED,
|
|
|
|
AUTO,
|
|
|
|
ALL,
|
|
|
|
ALL_NEXT_FRAME,
|
|
|
|
}
|
|
|
|
|
2021-07-08 15:10:58 +02:00
|
|
|
enum SelectionMode {
|
|
|
|
NONE,
|
|
|
|
POINTER,
|
|
|
|
}
|
|
|
|
|
2022-06-04 06:27:46 +02:00
|
|
|
export(UpdateMode) var update_mode = UpdateMode.AUTO setget set_update_mode
|
2021-06-19 15:57:05 +02:00
|
|
|
|
2022-08-15 00:35:01 +02:00
|
|
|
var cols = 2 setget _set_cols, _get_cols # Deprecated.
|
|
|
|
var rows = 2 setget _set_rows, _get_rows # Deprecated.
|
2021-06-19 15:57:05 +02:00
|
|
|
|
2021-07-08 15:33:46 +02:00
|
|
|
# If true, text in the terminal will be copied to the clipboard when selected.
|
2022-06-04 06:27:46 +02:00
|
|
|
export(bool) var copy_on_selection
|
2021-07-08 15:33:46 +02:00
|
|
|
|
2021-07-13 03:16:34 +02:00
|
|
|
# Bell
|
2021-07-21 17:58:20 +02:00
|
|
|
# If muted, the "bell" signal will not be emitted when the bell "\u0007" character
|
|
|
|
# is written to the terminal.
|
|
|
|
export var bell_muted := false
|
|
|
|
# Amount of time in seconds that must pass before emitting a new "bell" signal.
|
|
|
|
# This can be useful in cases where the bell character is being written too
|
|
|
|
# frequently such as `while true; do echo -e "\a"; done`.
|
|
|
|
export var bell_cooldown: float = 0.1
|
|
|
|
|
|
|
|
export var blink_on_time: float = 0.6
|
|
|
|
export var blink_off_time: float = 0.3
|
2021-07-13 03:16:34 +02:00
|
|
|
|
2022-08-15 00:35:01 +02:00
|
|
|
var _cols := 2
|
|
|
|
var _rows := 2
|
|
|
|
|
2021-07-25 19:31:23 +02:00
|
|
|
var _default_theme: Theme = preload("./themes/default.tres")
|
|
|
|
var _viewport: Viewport = preload("./nodes/terminal/viewport.tscn").instance()
|
2021-06-19 15:57:05 +02:00
|
|
|
var _native_terminal: Control = _viewport.get_node("Terminal")
|
|
|
|
var _screen := TextureRect.new()
|
2021-07-08 15:10:58 +02:00
|
|
|
|
2021-07-13 03:16:34 +02:00
|
|
|
var _bell_timer := Timer.new()
|
|
|
|
|
2021-07-08 15:10:58 +02:00
|
|
|
var _selecting := false
|
|
|
|
var _selecting_mode: int = SelectionMode.NONE
|
|
|
|
var _selection_timer := Timer.new()
|
|
|
|
|
2022-06-26 16:01:11 +02:00
|
|
|
var _buffer := []
|
|
|
|
|
2021-06-19 15:57:05 +02:00
|
|
|
|
|
|
|
func set_update_mode(value):
|
|
|
|
update_mode = value
|
|
|
|
_native_terminal.update_mode = value
|
|
|
|
|
|
|
|
|
2022-08-15 00:35:01 +02:00
|
|
|
func get_cols() -> int:
|
|
|
|
return _cols
|
|
|
|
|
|
|
|
|
|
|
|
func _get_cols() -> int:
|
|
|
|
push_warning(
|
|
|
|
"The 'cols' property of Terminal is deprecated and will be removed in a future version. Please use the `get_cols()` method instead."
|
|
|
|
)
|
|
|
|
return get_cols()
|
|
|
|
|
|
|
|
|
|
|
|
func _set_cols(_value) -> void:
|
|
|
|
push_error(
|
|
|
|
"The 'cols' property of Terminal is read-only and determined by rect_size and the theme's font size."
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-06-19 15:57:05 +02:00
|
|
|
func get_rows() -> int:
|
2022-08-15 00:35:01 +02:00
|
|
|
return _rows
|
2021-06-19 15:57:05 +02:00
|
|
|
|
|
|
|
|
2022-08-15 00:35:01 +02:00
|
|
|
func _get_rows() -> int:
|
|
|
|
push_warning(
|
|
|
|
"The 'rows' property of Terminal is deprecated and will be removed in a future version. Please use the `get_rows()` method instead."
|
|
|
|
)
|
|
|
|
return get_rows()
|
|
|
|
|
|
|
|
|
|
|
|
func _set_rows(_value) -> void:
|
|
|
|
push_error(
|
|
|
|
"The 'rows' property of Terminal is read-only and determined by rect_size and the theme's font size."
|
|
|
|
)
|
2021-06-19 15:57:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
func write(data) -> void:
|
2022-08-14 09:45:17 +02:00
|
|
|
assert(
|
|
|
|
data is PoolByteArray or data is String,
|
|
|
|
"Invalid type for argument 'data'. Should be of type PoolByteArray or String."
|
|
|
|
)
|
2021-07-02 19:04:34 +02:00
|
|
|
|
2022-06-26 16:01:11 +02:00
|
|
|
# Will be cleared when _flush() is called after VisualServer emits the "frame_pre_draw" signal.
|
|
|
|
_buffer.push_back(data)
|
|
|
|
|
2022-08-07 05:34:57 +02:00
|
|
|
# Ensure redraw is requested if terminal is visible.
|
|
|
|
if visible:
|
|
|
|
update()
|
|
|
|
|
2021-07-02 19:04:34 +02:00
|
|
|
|
2022-06-26 16:01:11 +02:00
|
|
|
func _flush():
|
|
|
|
for data in _buffer:
|
2022-08-14 09:45:17 +02:00
|
|
|
_native_terminal.write(data if data is PoolByteArray else data.to_utf8())
|
2022-06-26 16:01:11 +02:00
|
|
|
_native_terminal.update()
|
|
|
|
_buffer.clear()
|
2021-06-19 15:57:05 +02:00
|
|
|
|
|
|
|
|
2021-07-13 01:27:20 +02:00
|
|
|
func clear() -> void:
|
|
|
|
var initial_size = _native_terminal.rect_size
|
|
|
|
_native_terminal.rect_size.y = _native_terminal.cell_size.y
|
|
|
|
_native_terminal.clear_sb()
|
|
|
|
_native_terminal.rect_size = initial_size
|
|
|
|
|
|
|
|
|
2021-07-08 15:10:58 +02:00
|
|
|
func copy_selection() -> String:
|
|
|
|
return _native_terminal.copy_selection()
|
|
|
|
|
|
|
|
|
|
|
|
func copy_all() -> String:
|
|
|
|
return _native_terminal.copy_all()
|
|
|
|
|
|
|
|
|
2021-06-19 15:57:05 +02:00
|
|
|
func _ready():
|
2021-07-23 03:33:05 +02:00
|
|
|
_update_theme()
|
2021-06-19 15:57:05 +02:00
|
|
|
|
|
|
|
_native_terminal.update_mode = update_mode
|
|
|
|
_native_terminal.connect("data_sent", self, "_on_data_sent")
|
|
|
|
_native_terminal.connect("key_pressed", self, "_on_key_pressed")
|
|
|
|
_native_terminal.connect("size_changed", self, "_on_size_changed")
|
|
|
|
|
|
|
|
_viewport.size = rect_size
|
|
|
|
_viewport.render_target_update_mode = Viewport.UPDATE_ALWAYS
|
|
|
|
|
|
|
|
_screen.set_anchors_preset(PRESET_WIDE)
|
|
|
|
_screen.texture = _viewport.get_texture()
|
|
|
|
|
2021-07-13 03:16:34 +02:00
|
|
|
_native_terminal.connect("bell", self, "_on_bell")
|
|
|
|
_bell_timer.one_shot = true
|
|
|
|
add_child(_bell_timer)
|
|
|
|
|
2021-07-08 15:10:58 +02:00
|
|
|
_selection_timer.wait_time = 0.05
|
|
|
|
_selection_timer.connect("timeout", self, "_on_selection_held")
|
|
|
|
|
2021-06-19 15:57:05 +02:00
|
|
|
add_child(_viewport)
|
|
|
|
add_child(_screen)
|
2021-07-08 15:10:58 +02:00
|
|
|
add_child(_selection_timer)
|
2021-06-19 15:57:05 +02:00
|
|
|
|
|
|
|
_refresh()
|
|
|
|
|
2022-06-26 16:01:11 +02:00
|
|
|
# Ensure the terminal state machine's framebuffer is up to date before
|
|
|
|
# we make all the draw_* calls caused by writing. We need to use signals
|
|
|
|
# here rather than yield otherwise we will sometimes get a "Resumed
|
|
|
|
# function after yield but class instance is gone" error.
|
|
|
|
VisualServer.connect("frame_pre_draw", self, "_flush")
|
|
|
|
|
2021-06-19 15:57:05 +02:00
|
|
|
|
2021-07-23 03:33:05 +02:00
|
|
|
func _update_theme():
|
|
|
|
# Themes are not propagated through the Viewport, so in order for theme
|
|
|
|
# inheritance to work we can pass through the theme variables manually.
|
|
|
|
for color in _default_theme.get_color_list("Terminal"):
|
|
|
|
var c: Color
|
2022-08-23 13:58:24 +02:00
|
|
|
|
|
|
|
var deprecated_color: String = color.replace("bright", "light").replace("_", " ").capitalize()
|
|
|
|
match deprecated_color:
|
|
|
|
"White":
|
|
|
|
deprecated_color = "Light Grey"
|
|
|
|
"Light Black":
|
|
|
|
deprecated_color = "Dark Grey"
|
|
|
|
"Light White":
|
|
|
|
deprecated_color = "Wahite"
|
|
|
|
|
2021-07-23 03:33:05 +02:00
|
|
|
if has_color(color, "Terminal"):
|
|
|
|
c = get_color(color, "Terminal")
|
2022-08-23 13:58:24 +02:00
|
|
|
elif has_color(deprecated_color, "Terminal"):
|
|
|
|
push_warning(
|
|
|
|
(
|
|
|
|
"Color name '%s' is deprecated and will be removed it a future version. Use the name '%s' instead."
|
|
|
|
% [deprecated_color, color]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
c = get_color(deprecated_color, "Terminal")
|
2021-07-23 03:33:05 +02:00
|
|
|
else:
|
|
|
|
c = _default_theme.get_color(color, "Terminal")
|
|
|
|
_native_terminal.add_color_override(color, c)
|
|
|
|
for font in _default_theme.get_font_list("Terminal"):
|
|
|
|
var f: Font
|
2022-08-23 13:58:24 +02:00
|
|
|
var deprecated_font: String = font.replace("_", " ").capitalize()
|
|
|
|
|
2021-07-23 03:33:05 +02:00
|
|
|
if has_font(font, "Terminal"):
|
|
|
|
f = get_font(font, "Terminal")
|
2022-08-23 13:58:24 +02:00
|
|
|
elif has_font(deprecated_font, "Terminal"):
|
|
|
|
push_warning(
|
|
|
|
(
|
|
|
|
"Font name '%s' is deprecated and will be removed in a future version. Use the name '%s' instead."
|
|
|
|
% [deprecated_font, font]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
f = get_font(deprecated_font, "Terminal")
|
|
|
|
elif has_font("regular", "Terminal"):
|
|
|
|
f = get_font("regular", "Terminal")
|
2021-07-23 03:33:05 +02:00
|
|
|
else:
|
|
|
|
if _default_theme.has_font(font, "Terminal"):
|
|
|
|
f = _default_theme.get_font(font, "Terminal")
|
|
|
|
else:
|
2022-08-23 13:58:24 +02:00
|
|
|
f = _default_theme.get_font(font, "regular")
|
2021-07-23 03:33:05 +02:00
|
|
|
_native_terminal.add_font_override(font, f)
|
|
|
|
_native_terminal._update_theme()
|
|
|
|
_native_terminal._update_size()
|
|
|
|
|
|
|
|
|
2021-06-19 15:57:05 +02:00
|
|
|
func _refresh():
|
2021-07-11 17:32:13 +02:00
|
|
|
_screen.update()
|
2021-06-19 15:57:05 +02:00
|
|
|
if update_mode == UpdateMode.AUTO:
|
|
|
|
_native_terminal.update_mode = UpdateMode.ALL_NEXT_FRAME
|
|
|
|
|
|
|
|
|
|
|
|
func _gui_input(event):
|
|
|
|
_native_terminal._gui_input(event)
|
2021-07-13 01:27:20 +02:00
|
|
|
|
2022-06-26 19:41:24 +02:00
|
|
|
if event is InputEventKey and event.pressed:
|
|
|
|
# Return to bottom of scrollback buffer if we scrolled up. Ignore modifier
|
|
|
|
# keys pressed in isolation or if Ctrl+Shift modifier keys are pressed.
|
|
|
|
if (
|
|
|
|
not event.scancode in [KEY_ALT, KEY_SHIFT, KEY_CONTROL, KEY_META, KEY_MASK_CMD]
|
|
|
|
and not (event.control and event.shift)
|
|
|
|
):
|
|
|
|
_native_terminal.sb_reset()
|
2021-07-13 01:27:20 +02:00
|
|
|
|
2022-08-03 12:07:21 +02:00
|
|
|
# Prevent focus changing to other inputs when pressing Tab or Arrow keys.
|
|
|
|
if event.scancode in [KEY_LEFT, KEY_UP, KEY_RIGHT, KEY_DOWN, KEY_TAB]:
|
|
|
|
accept_event()
|
|
|
|
|
2021-07-08 15:10:34 +02:00
|
|
|
_handle_mouse_wheel(event)
|
2021-07-08 15:10:58 +02:00
|
|
|
_handle_selection(event)
|
2021-07-08 15:10:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
func _handle_mouse_wheel(event: InputEventMouseButton):
|
|
|
|
if not event or not event.is_pressed():
|
|
|
|
return
|
|
|
|
|
|
|
|
if event.button_index == BUTTON_WHEEL_UP:
|
|
|
|
if event.alt:
|
|
|
|
# Scroll 5 times as fast as normal (like TextEdit).
|
|
|
|
_native_terminal.sb_up(15 * event.factor)
|
|
|
|
else:
|
|
|
|
# Scroll 3 lines.
|
|
|
|
_native_terminal.sb_up(3 * event.factor)
|
|
|
|
|
|
|
|
if event.button_index == BUTTON_WHEEL_DOWN:
|
|
|
|
if event.alt:
|
|
|
|
# Scroll 5 times as fast as normal (like TextEdit).
|
|
|
|
_native_terminal.sb_down(15 * event.factor)
|
|
|
|
else:
|
|
|
|
# Scroll 3 lines.
|
|
|
|
_native_terminal.sb_down(3 * event.factor)
|
2021-06-19 15:57:05 +02:00
|
|
|
|
|
|
|
|
2021-07-08 15:10:58 +02:00
|
|
|
func _handle_selection(event: InputEventMouse):
|
|
|
|
if event is InputEventMouseButton:
|
|
|
|
if not event or not event.is_pressed() or not event.button_index == BUTTON_LEFT:
|
|
|
|
return
|
|
|
|
|
|
|
|
if _selecting:
|
|
|
|
_selecting = false
|
|
|
|
_selecting_mode = SelectionMode.NONE
|
|
|
|
_native_terminal.reset_selection()
|
|
|
|
|
|
|
|
# Single-click select pointer.
|
|
|
|
_selecting = false
|
|
|
|
_selecting_mode = SelectionMode.POINTER
|
|
|
|
|
|
|
|
elif event is InputEventMouseMotion:
|
|
|
|
if (
|
|
|
|
event.button_mask & BUTTON_MASK_LEFT
|
|
|
|
and _selecting_mode != SelectionMode.NONE
|
|
|
|
and not _selecting
|
|
|
|
):
|
|
|
|
_selecting = true
|
|
|
|
_native_terminal.start_selection(_mouse_to_cell(event.position))
|
|
|
|
_selection_timer.start()
|
|
|
|
|
|
|
|
|
|
|
|
func _on_selection_held() -> void:
|
|
|
|
if not Input.is_mouse_button_pressed(BUTTON_LEFT) or _selecting_mode == SelectionMode.NONE:
|
2021-07-08 15:33:46 +02:00
|
|
|
if copy_on_selection:
|
|
|
|
var selection = _native_terminal.copy_selection()
|
|
|
|
OS.set_clipboard(selection)
|
2021-07-08 15:10:58 +02:00
|
|
|
_selection_timer.stop()
|
|
|
|
return
|
|
|
|
|
|
|
|
var position: Vector2 = _mouse_to_cell(get_local_mouse_position())
|
|
|
|
_native_terminal.select_to_pointer(position)
|
|
|
|
_selection_timer.start()
|
|
|
|
|
|
|
|
|
2021-06-19 15:57:05 +02:00
|
|
|
func _notification(what: int) -> void:
|
|
|
|
match what:
|
|
|
|
NOTIFICATION_RESIZED:
|
|
|
|
_viewport.size = rect_size
|
|
|
|
_refresh()
|
|
|
|
NOTIFICATION_THEME_CHANGED:
|
2021-07-23 03:33:05 +02:00
|
|
|
_update_theme()
|
2021-06-19 15:57:05 +02:00
|
|
|
_refresh()
|
|
|
|
|
|
|
|
|
|
|
|
func _on_data_sent(data: PoolByteArray):
|
|
|
|
emit_signal("data_sent", data)
|
|
|
|
|
|
|
|
|
2022-08-14 09:45:17 +02:00
|
|
|
func _on_key_pressed(data: PoolByteArray, event: InputEventKey):
|
|
|
|
emit_signal("key_pressed", data.get_string_from_utf8(), event)
|
2021-06-19 15:57:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
func _on_size_changed(new_size: Vector2):
|
2022-08-15 00:35:01 +02:00
|
|
|
_cols = new_size.x
|
|
|
|
_rows = new_size.y
|
2021-06-19 15:57:05 +02:00
|
|
|
emit_signal("size_changed", new_size)
|
2021-07-02 19:04:34 +02:00
|
|
|
|
|
|
|
|
2021-07-03 16:57:19 +02:00
|
|
|
func _on_bell():
|
2021-07-21 17:58:20 +02:00
|
|
|
if not bell_muted and (bell_cooldown == 0 or _bell_timer.time_left == 0):
|
|
|
|
emit_signal("bell")
|
|
|
|
if bell_cooldown > 0:
|
|
|
|
_bell_timer.start(bell_cooldown)
|
2021-07-13 03:16:34 +02:00
|
|
|
|
|
|
|
|
2021-07-08 15:10:58 +02:00
|
|
|
func _mouse_to_cell(pos: Vector2) -> Vector2:
|
|
|
|
return Vector2(pos / _native_terminal.cell_size)
|