mirror of
https://github.com/lihop/godot-xterm.git
synced 2025-05-04 20:24:23 +02:00
Move terminal.gd and pty.gd to godot_xterm directory
Makes for pretty paths when extending scripts: `extends "res://addons/godot_xterm/terminal.gd"` vs. `extends "res://addons/godot_xterm/nodes/terminal/terminal.gd"` Currently "res://addons/godot_xterm/pty.gd" is acutally `pty_unix.gd`. This is okay for now as the PTY node is only supported on Unix platforms. However, we will need to sort it out when adding Windows support as part of #25. Also remove the GDXterm namespace.
This commit is contained in:
parent
e6db81615e
commit
9d15420df3
22 changed files with 46 additions and 43 deletions
|
@ -9,7 +9,7 @@ extends Node
|
|||
|
||||
const LibuvUtils := preload("./libuv_utils.gd")
|
||||
const Pipe := preload("./pipe.gdns")
|
||||
const Terminal := preload("../terminal/terminal.gd")
|
||||
const Terminal := preload("../../terminal.gd")
|
||||
|
||||
const DEFAULT_NAME := "xterm-256color"
|
||||
const DEFAULT_COLS := 80
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
# Derived from https://github.com/microsoft/node-pty/blob/main/src/unixTerminal.ts
|
||||
# Copyright (c) 2012-2015, Christopher Jeffrey (MIT License).
|
||||
# Copyright (c) 2016, Daniel Imms (MIT License).
|
||||
# Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||
# Copyright (c) 2021, Leroy Hopson (MIT License).
|
||||
|
||||
tool
|
||||
extends "../pty.gd"
|
||||
|
||||
const PTYUnix = preload("./pty_unix.gdns")
|
||||
|
||||
const FALLBACK_FILE = "sh"
|
||||
|
||||
# Security warning: use this option with great caution, as opened file descriptors
|
||||
# with higher privileges might leak to the child program.
|
||||
var uid: int
|
||||
var gid: int
|
||||
|
||||
var thread: Thread
|
||||
|
||||
var _fd: int = -1
|
||||
var _exit_cb: FuncRef
|
||||
|
||||
#static func get_uid() -> int:
|
||||
# return -1 # Not implemented.
|
||||
|
||||
#static func get_gid() -> int:
|
||||
# return -1 # Not implemented.
|
||||
|
||||
|
||||
func _resize(cols: int, rows: int) -> void:
|
||||
if _fd < 0:
|
||||
return
|
||||
|
||||
PTYUnix.new().resize(_fd, cols, rows)
|
||||
|
||||
|
||||
func _fork_thread(args):
|
||||
var result = preload("./pty_unix.gdns").new().callv("fork", args)
|
||||
return result
|
||||
|
||||
|
||||
func fork(
|
||||
file: String = OS.get_environment("SHELL"),
|
||||
args: PoolStringArray = PoolStringArray(),
|
||||
cwd = LibuvUtils.get_cwd(),
|
||||
p_cols: int = DEFAULT_COLS,
|
||||
p_rows: int = DEFAULT_ROWS,
|
||||
uid: int = -1,
|
||||
gid: int = -1,
|
||||
utf8 = true
|
||||
) -> int:
|
||||
# File.
|
||||
if file.empty():
|
||||
file = FALLBACK_FILE
|
||||
|
||||
# Environment variables.
|
||||
# If we are using OS env vars, sanitize them to remove variables that might confuse our terminal.
|
||||
var final_env := _sanitize_env(LibuvUtils.get_os_environ()) if use_os_env else {}
|
||||
for key in env.keys():
|
||||
final_env[key] = env[key]
|
||||
var parsed_env: PoolStringArray = _parse_env(final_env)
|
||||
|
||||
# Exit callback.
|
||||
_exit_cb = FuncRef.new()
|
||||
_exit_cb.set_instance(self)
|
||||
_exit_cb.set_function("_on_exit")
|
||||
|
||||
# Actual fork.
|
||||
var result = PTYUnix.new().fork( # VERY IMPORTANT: The must be set null or 0, otherwise will get an ENOTSOCK error after connecting our pipe to the fd.
|
||||
file, null, args, parsed_env, cwd, cols, rows, uid, gid, utf8, _exit_cb
|
||||
)
|
||||
|
||||
if result[0] != OK:
|
||||
push_error("Fork failed.")
|
||||
return FAILED
|
||||
|
||||
_fd = result[1].fd
|
||||
if _fd < 0:
|
||||
push_error("File descriptor must be a non-negative integer value.")
|
||||
return FAILED
|
||||
|
||||
_pid = result[1].pid
|
||||
|
||||
_pipe = Pipe.new()
|
||||
_pipe.open(_fd)
|
||||
|
||||
# Must connect to signal AFTER opening, otherwise we will get error ENOTSOCK.
|
||||
_pipe.connect("data_received", self, "_on_pipe_data_received")
|
||||
|
||||
return OK
|
||||
|
||||
|
||||
func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> Array:
|
||||
return PTYUnix.new().open(cols, rows)
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
_exit_cb = null
|
||||
if _pid > 1:
|
||||
LibuvUtils.kill(_pid, Signal.SIGHUP)
|
||||
|
||||
|
||||
func _on_pipe_data_received(data):
|
||||
emit_signal("data_received", data)
|
||||
|
||||
|
||||
func _on_exit(exit_code: int, signum: int) -> void:
|
||||
if is_instance_valid(self):
|
||||
_pid = -1
|
||||
emit_signal("exited", exit_code, signum)
|
||||
|
||||
|
||||
func _sanitize_env(env: Dictionary) -> Dictionary:
|
||||
# Make sure we didn't start our server from inside tmux.
|
||||
env.erase("TMUX")
|
||||
env.erase("TMUX_PANE")
|
||||
|
||||
# Make sure we didn't start our server from inside screen.
|
||||
# http://web.mit.edu/gnu/doc/html/screen_20.html
|
||||
env.erase("STY")
|
||||
env.erase("WINDOW")
|
||||
|
||||
# Delete some variables that might confuse our terminal.
|
||||
env.erase("WINDOWID")
|
||||
env.erase("TERMCAP")
|
||||
env.erase("COLUMNS")
|
||||
env.erase("LINES")
|
||||
|
||||
return env
|
|
@ -1,272 +0,0 @@
|
|||
# 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
|
||||
|
||||
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 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
|
||||
|
||||
var _default_theme: Theme = preload("../../themes/default.tres")
|
||||
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():
|
||||
_update_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 _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
|
||||
if has_color(color, "Terminal"):
|
||||
c = get_color(color, "Terminal")
|
||||
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
|
||||
if has_font(font, "Terminal"):
|
||||
f = get_font(font, "Terminal")
|
||||
elif has_font("Regular", "Terminal"):
|
||||
f = get_font("Regular", "Terminal")
|
||||
else:
|
||||
if _default_theme.has_font(font, "Terminal"):
|
||||
f = _default_theme.get_font(font, "Terminal")
|
||||
else:
|
||||
f = _default_theme.get_font(font, "Regular")
|
||||
_native_terminal.add_font_override(font, f)
|
||||
_native_terminal._update_theme()
|
||||
_native_terminal._update_size()
|
||||
|
||||
|
||||
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:
|
||||
_update_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 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)
|
||||
|
||||
|
||||
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."
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue