godot-xterm/addons/godot_xterm/nodes/pty/pty.gd
Leroy Hopson 115521f645
Replace Gut with WAT
Gut was freezing on some integration tests. It was also entering an
infinite loop after exiting (even after closing Godot and VSCode) which
caused a `godot.log` file in app_userdata to keep growing until my hard
drive was full.
2021-07-03 21:49:24 +07:00

262 lines
7.7 KiB
GDScript

# Derived from https://github.com/microsoft/node-pty/blob/main/src/terminal.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 Node
const LibuvUtils := preload("./libuv_utils.gd")
const Pipe := preload("./pipe.gdns")
const Terminal := preload("../terminal/terminal.gd")
const DEFAULT_NAME := "xterm-256color"
const DEFAULT_COLS := 80
const DEFAULT_ROWS := 24
const DEFAULT_ENV := {TERM = DEFAULT_NAME, COLORTERM = "truecolor"}
## Default messages to indicate PAUSE/RESUME for automatic flow control.
## To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh),
## the sequences can be customized in IPtyForkOptions.
#const FLOW_CONTROL_PAUSE = char(0x13) # defaults to XOFF
#const FLOW_CONTROL_RESUME = char(0x11) # defaults to XON
enum Status { NONE, OPEN, EXITED, ERROR }
const STATUS_NONE = Status.NONE
const STATUS_OPEN = Status.OPEN
const STATUS_EXITED = Status.EXITED
const STATUS_ERROR = Status.ERROR
# Any signal_number can be sent to the pty's process using the kill() function,
# these are just the signals with numbers specified in the POSIX standard.
enum Signal {
SIGHUP = 1, # Hangup
SIGINT = 2, # Terminal interrupt signal
SIGQUIT = 3, # Terminal quit signal
SIGILL = 4, # Illegal instruction
SIGTRAP = 5, # Trace/breakpoint trap
SIGABRT = 6, # Process abort signal
SIGFPE = 8, # Erroneous arithmetic operation
SIGKILL = 9, # Kill (cannot be caught or ignored)
SIGSEGV = 11, # Invalid memory reference
SIGPIPE = 13, # Write on a pipe with no one to read it
SIGALRM = 14, # Alarm clock
SIGTERM = 15, # Termination signal
}
signal data_received(data)
signal exited(exit_code, signum)
signal errored(message)
export (NodePath) var terminal_path := NodePath() setget set_terminal_path
var status := STATUS_NONE
var error_string := ""
var terminal: Terminal = null setget set_terminal
## Name of the terminal to be set in environment ($TERM variable).
#export (String) var term_name: String
# The name of the process.
var process: String
# The process ID.
var pid: int
# The column size in characters.
export (int) var cols: int = DEFAULT_COLS setget set_cols
# The row size in characters.
export (int) var rows: int = DEFAULT_ROWS setget set_rows
# Working directory to be set for the child program.
#export (String) var cwd := LibuvUtils.get_cwd()
# Environment to be set for the child program.
export (Dictionary) var env := DEFAULT_ENV
# If true the environment variables in the env Dictionary will be merged with
# the environment variables of the operating system (e.g. printenv), with the
# former taking precedence in the case of conflicts.
export (bool) var use_os_env := true
# If true, pty will call fork() in in _ready().
export (bool) var autostart := false
# (EXPERIMENTAL)
# If true, PTY node will create a blocking libuv loop in a new thread.
# signals will be emitted using call_deferred.
#export (bool) var use_threads := false
## String encoding of the underlying pty.
## If set, incoming data will be decoded to strings and outgoing strings to bytes applying this encoding.
## If unset, incoming data will be delivered as raw bytes (PoolByteArray type).
## By default 'utf8' is assumed, to unset it explicitly set it to `null`.
#var encoding: String = "utf8"
## (EXPERIMENTAL)
## Whether to enable flow control handling (false by default). If enabled a message of `flow_control_pause`
## will pause the socket and thus blocking the child program execution due to buffer back pressure.
## A message of `flow_control_resume` will resume the socket into flow mode.
## For performance reasons only a single message as a whole will match (no message part matching).
## If flow control is enabled the `flow_control_pause` and `flow_control_resume` messages are not forwarded to
## the underlying pseudoterminal.
#var handle_flow_control: bool = false
#
## (EXPERIMENTAL)
## The string that should pause the pty when `handle_flow_control` is true. Default is XOFF ("\u0013").
#var flow_control_pause: String = FLOW_CONTROL_PAUSE
#
## (EXPERIMENTAL)
## The string that should resume the pty when `handle_flow_control` is true. Default is XON ("\u0011").
#var flow_control_resume: String = FLOW_CONTROL_RESUME
#var _native_term # Platform appropriate instance of this class.
var _pipe: Pipe
func set_cols(value: int):
resize(value, rows)
func set_rows(value: int):
resize(cols, value)
func set_terminal_path(value := NodePath()):
terminal_path = value
set_terminal(get_node_or_null(terminal_path))
func set_terminal(value: Terminal):
if terminal == value:
return
# Disconect the current terminal, if any.
if terminal:
disconnect("data_received", terminal, "write")
terminal.disconnect("data_sent", self, "write")
terminal.disconnect("size_changed", self, "resize")
terminal = value
if not terminal:
return
# Connect the new terminal.
# FIXME! resize(terminal.get_cols(), terminal.get_rows())
if not terminal.is_connected("size_changed", self, "resize"):
terminal.connect("size_changed", self, "resize")
if not terminal.is_connected("data_sent", self, "write"):
terminal.connect("data_sent", self, "write")
if not is_connected("data_received", terminal, "write"):
connect("data_received", terminal, "write")
# Writes data to the socket.
# data: The data to write.
func write(data) -> void:
assert(data is String or data is PoolByteArray)
if data is PoolByteArray:
data = data.get_string_from_utf8()
# if handle_flow_control:
# # PAUSE/RESUME messages are not forwarded to the pty.
# if data == flow_control_pause:
# pause()
# return
# if data == flow_control_resume:
# resume()
# return
# # Everything else goes to the real pty.
_write(data)
func _write(data: String) -> void:
if _pipe:
_pipe.write(data)
# Resizes the dimensions of the pty.
# cols: The number of columns.
# rows: The number of rows.
# Also accepts a single Vector2 argument where x is the the number of columns
# and y is the number of rows.
func resize(cols, rows = null) -> void:
assert(
(cols is Vector2 and rows == null) or (cols is int and rows is int),
"Usage: resize(size: Vector2) or resize(cols: int, rows: int)"
)
if cols is Vector2:
rows = cols.y # Must get rows before reassigning cols!
cols = cols.x
if cols <= 0 or rows <= 0 or cols == NAN or rows == NAN or cols == INF or rows == INF:
push_error("Resizing must be done using positive cols and rows.")
_resize(cols, rows)
func _resize(cols: int, rows: int) -> void:
assert(false, "Not implemented.")
# Close, kill and destroy the pipe.
func destroy() -> void:
pass
# Kill the pty.
# sigint: The signal to send. By default this is SIGHUP.
# This is not supported on Windows.
func kill(sigint: int = Signal.SIGHUP) -> void:
pass
func fork(
p_file: String,
p_args = PoolStringArray(),
p_cwd: String = LibuvUtils.get_cwd(),
p_cols: int = DEFAULT_COLS,
p_rows: int = DEFAULT_ROWS
):
push_error("Not implemented.")
func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> Array:
assert(false, "Not implemented.")
return [FAILED]
func _parse_env(env: Dictionary = {}) -> PoolStringArray:
var keys := env.keys()
var pairs := PoolStringArray()
for key in keys:
var value = env[key]
var valid = key is String and value is String
assert(valid, "Env key/value pairs must be of type String/String.")
if not valid:
push_warning("Skipping invalid env key/value pair.")
continue
pairs.append("%s=%s" % [key, value])
return pairs
func _process(_delta):
if _pipe:
_pipe.poll()
func _notification(what: int):
match what:
NOTIFICATION_PARENTED:
var parent = get_parent()
if parent is Terminal:
set_terminal_path(get_path_to(parent))