mirror of
https://github.com/lihop/godot-xterm.git
synced 2024-11-12 21:50:26 +01:00
115521f645
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.
262 lines
7.7 KiB
GDScript
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))
|