godot-xterm/addons/godot_xterm/nodes/terminal/terminal.gd
Leroy Hopson b08d218a59
Run tests in CI environment
Currently tests only run on X11.64 platform.
But other platforms can be supported with a bit of effort.
Remove default bell sound as it does not play nicely with CI environment
that does not have sound card.
2021-07-18 23:05:17 +07:00

256 lines
6.6 KiB
GDScript

# 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).
tool
extends Control
const DefaultTheme = preload("../../themes/default.tres")
signal data_sent(data)
signal key_pressed(data, event)
signal size_changed(new_size)
signal bell
enum UpdateMode {
DISABLED,
AUTO,
ALL,
ALL_NEXT_FRAME,
}
enum SelectionMode {
NONE,
POINTER,
}
export (UpdateMode) var update_mode = UpdateMode.AUTO setget set_update_mode
var cols = 2
var rows = 2
# If true, text in the terminal will be copied to the clipboard when selected.
export (bool) var copy_on_selection
# Bell
# If enabled, bell_sound will play when the ASCII BELL "\u0007" character is printed.
export var bell_enabled := true
export var bell_sound: AudioStream
# Number of milliseconds that must pass before emitting a new bell sound.
# This important in cases where the bell character is being printed frequently
# such as `while true; do echo -e "\a"; done`, as adding additional AudioStreamPlayer
# nodes too frequently has a negative performance impact.
export var bell_cooldown_msec: int = 100
var _viewport: Viewport = preload("./viewport.tscn").instance()
var _native_terminal: Control = _viewport.get_node("Terminal")
var _screen := TextureRect.new()
var _bell_timer := Timer.new()
var _selecting := false
var _selecting_mode: int = SelectionMode.NONE
var _selection_timer := Timer.new()
var _dirty := false
var buffer := StreamPeerBuffer.new()
var times = 0
func set_update_mode(value):
update_mode = value
_native_terminal.update_mode = value
func get_rows() -> int:
return 0
func get_cols() -> int:
return 0
func write(data) -> void:
assert(data is String or data is PoolByteArray)
# FIXME: This will occasionally cause a "Resumed function after yield, but class instance is gone" error after freeing the Terminal instance.
# However, this yield is necessary to ensure the terminal state machines framebuffer is up to date when we make all the draw_* calls.
yield(VisualServer, "frame_pre_draw")
_native_terminal.write(data if data is String else data.get_string_from_utf8())
_native_terminal.update()
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
func copy_selection() -> String:
return _native_terminal.copy_selection()
func copy_all() -> String:
return _native_terminal.copy_all()
func _ready():
if theme:
_native_terminal.theme = theme
_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()
_native_terminal.connect("bell", self, "_on_bell")
_bell_timer.one_shot = true
add_child(_bell_timer)
_selection_timer.wait_time = 0.05
_selection_timer.connect("timeout", self, "_on_selection_held")
add_child(_viewport)
add_child(_screen)
add_child(_selection_timer)
_refresh()
func _refresh():
_screen.update()
if update_mode == UpdateMode.AUTO:
_native_terminal.update_mode = UpdateMode.ALL_NEXT_FRAME
func _gui_input(event):
_native_terminal._gui_input(event)
if event is InputEventKey:
_native_terminal.sb_reset() # Return to bottom of scrollback buffer if we scrolled up.
_handle_mouse_wheel(event)
_handle_selection(event)
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)
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:
if copy_on_selection:
var selection = _native_terminal.copy_selection()
OS.set_clipboard(selection)
_selection_timer.stop()
return
var position: Vector2 = _mouse_to_cell(get_local_mouse_position())
_native_terminal.select_to_pointer(position)
_selection_timer.start()
func _notification(what: int) -> void:
match what:
NOTIFICATION_RESIZED:
_viewport.size = rect_size
_refresh()
NOTIFICATION_THEME_CHANGED:
_native_terminal.theme = theme
_refresh()
func _on_data_sent(data: PoolByteArray):
emit_signal("data_sent", data)
func _on_key_pressed(data: String, event: InputEventKey):
emit_signal("key_pressed", data, event)
func _on_size_changed(new_size: Vector2):
cols = new_size.x
rows = new_size.y
emit_signal("size_changed", new_size)
func _on_bell():
if bell_enabled and bell_sound and _bell_timer.time_left == 0:
var player := AudioStreamPlayer.new()
player.stream = bell_sound
player.autoplay = true
player.playing = true
player.connect("finished", self, "_on_player_finished", [player])
add_child(player)
_bell_timer.start(0.001 * bell_cooldown_msec)
emit_signal("bell")
func _on_player_finished(player: AudioStreamPlayer):
player.queue_free()
func _mouse_to_cell(pos: Vector2) -> Vector2:
return Vector2(pos / _native_terminal.cell_size)
func _set_size_warning(value):
if value:
push_warning(
"Terminal cols and rows are read only and determined by the font and rect sizes."
)