mirror of
https://github.com/lihop/godot-xterm.git
synced 2025-02-18 21:03:57 +01:00
Remove gdscript version and replace with native
This commit is contained in:
parent
d8c8b5b272
commit
f9474fe533
126 changed files with 116 additions and 14221 deletions
8
.gitmodules
vendored
8
.gitmodules
vendored
|
@ -1,6 +1,6 @@
|
|||
[submodule "addons/godot_xterm_native/godot-cpp"]
|
||||
path = addons/godot_xterm_native/godot-cpp
|
||||
[submodule "addons/godot_xterm/godot-cpp"]
|
||||
path = addons/godot_xterm/godot-cpp
|
||||
url = https://github.com/lihop/godot-cpp
|
||||
[submodule "addons/godot_xterm_native/libtsm"]
|
||||
path = addons/godot_xterm_native/libtsm
|
||||
[submodule "addons/godot_xterm/libtsm"]
|
||||
path = addons/godot_xterm/libtsm
|
||||
url = https://github.com/Aetf/libtsm
|
||||
|
|
|
@ -5,13 +5,11 @@ services:
|
|||
|
||||
jobs:
|
||||
include:
|
||||
- name: "Run tests"
|
||||
env: SERVICE=tests
|
||||
- name: "Build native on Arch Linux"
|
||||
- name: "Build on Arch Linux"
|
||||
env: SERVICE=build-archlinux
|
||||
- name: "Build native on NixOS"
|
||||
- name: "Build on NixOS"
|
||||
env: SERVICE=build-nixos
|
||||
- name: "Build native on Ubuntu"
|
||||
- name: "Build on Ubuntu"
|
||||
env: SERVICE=build-ubuntu
|
||||
|
||||
script: docker-compose run $SERVICE
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
|
||||
Native implementation of Godot Xterm using GDNative with [libtsm](https://github.com/Aetf/libtsm).
|
||||
|
||||
Terminal emulator for Godot using GDNative and [libtsm](https://github.com/Aetf/libtsm).
|
||||
|
||||
**Note**: If you are looking for the purely gdscript version of this plugin it was too buggy and slow so is no longer being developed or maintained but can found [here](https://github.com/lihop/godot-xterm/tree/gdscript-unmaintained).
|
||||
|
||||
## Demo
|
||||
|
||||
Click the thumbnail to watch a demo video on youtube:
|
||||
|
@ -22,4 +26,4 @@ You are also implicitly verifying that all code is your original work, or unorig
|
|||
Copyright (c) 2020 Leroy Hopson (MIT License)
|
||||
|
||||
The fonts used in this project are published under a seperate license.
|
||||
See: [addons/godot_xterm_native/fonts/LICENSE.txt](addons/godot_xterm_native/fonts/LICENSE.txt).
|
||||
See: [addons/godot_xterm/fonts/LICENSE.txt](addons/godot_xterm/fonts/LICENSE.txt).
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
extends Reference
|
||||
|
||||
# font flags
|
||||
enum {
|
||||
FONT_NORMAL = 0,
|
||||
FONT_BOLD = 1 << 0
|
||||
FONT_ITALIC = 1 << 1,
|
||||
FONT_UNDERLINED = 1 << 2
|
||||
FONT_BLINK = 1 << 3
|
||||
FONT_INVERSE = 1 << 4
|
||||
FONT_IVSIBILE = 1 << 5
|
||||
FONT_STRIKETHROUGH = 1 << 6
|
||||
}
|
||||
|
||||
# colors
|
||||
const COLOR_BLACK = Color(0.0, 0.0, 0.0) # 0
|
||||
const COLOR_RED = Color(1.0, 0.0, 0.0) # 1
|
||||
const COLOR_GREEN = Color(0.0, 1.0, 0.0) # 2
|
||||
const COLOR_YELLOW = Color(1.0, 1.0, 0.0) # 3
|
||||
const COLOR_BLUE = Color(0.0, 0.0, 1.0) # 4
|
||||
const COLOR_MAGENTA = Color(1.0, 0.0, 1.0) # 5
|
||||
const COLOR_CYAN = Color(0.0, 1.0, 1.0) # 6
|
||||
const COLOR_WHITE = Color(1.0, 1.0, 1.0) # 7
|
|
@ -1,168 +0,0 @@
|
|||
# Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
|
||||
const Attributes = Constants.Attributes
|
||||
const FgFlags = Constants.FgFlags
|
||||
const BgFlags = Constants.BgFlags
|
||||
const UnderlineStyle = Constants.UnderlineStyle
|
||||
|
||||
var fg = 0
|
||||
var bg = 0
|
||||
var extended = ExtendedAttrs.new()
|
||||
|
||||
|
||||
static func to_color_rgb(value: int) -> Color:
|
||||
# Create color from RGB format.
|
||||
return Color("%02x%02x%02x" % [value >> Attributes.RED_SHIFT & 255,
|
||||
value >> Attributes.GREEN_SHIFT & 255, value & 255])
|
||||
|
||||
|
||||
# flags
|
||||
func is_inverse() -> int:
|
||||
return fg & FgFlags.INVERSE
|
||||
func is_bold() -> int:
|
||||
return fg & FgFlags.BOLD
|
||||
func is_underline() -> int:
|
||||
return fg & FgFlags.UNDERLINE
|
||||
func is_blink() -> int:
|
||||
return fg & FgFlags.BLINK
|
||||
func is_invisible() -> int:
|
||||
return fg & FgFlags.INVISIBLE
|
||||
func is_italic() -> int:
|
||||
return bg & BgFlags.ITALIC
|
||||
func is_dim() -> int:
|
||||
return fg & BgFlags.DIM
|
||||
|
||||
|
||||
# color modes
|
||||
func get_fg_color_mode() -> int:
|
||||
return fg & Attributes.CM_MASK
|
||||
func get_bg_color_mode() -> int:
|
||||
return bg & Attributes.CM_MASK
|
||||
func is_fg_rgb() -> bool:
|
||||
return (fg & Attributes.CM_MASK) == Attributes.CM_RGB
|
||||
func is_bg_rgb() -> bool:
|
||||
return (bg & Attributes.CM_MASK) == Attributes.CM_RGB
|
||||
func is_fg_palette() -> bool:
|
||||
return (fg & Attributes.CM_MASK) == Attributes.CM_P16 or (fg & Attributes.CM_MASK) == Attributes.CM_P256
|
||||
func is_bg_palette() -> bool:
|
||||
return (bg & Attributes.CM_MASK) == Attributes.CM_P16 or (bg & Attributes.CM_MASK) == Attributes.CM_P256
|
||||
func is_fg_default() -> bool:
|
||||
return (fg & Attributes.CM_MASK) == 0
|
||||
func is_bg_default() -> bool:
|
||||
return (bg & Attributes.CM_MASK) == 0
|
||||
func is_attribute_default() -> bool:
|
||||
return fg == 0 && bg == 0
|
||||
|
||||
|
||||
func get_fg_color() -> int:
|
||||
match fg & Attributes.CM_MASK:
|
||||
Attributes.CM_P16, Attributes.CM_P256:
|
||||
return fg & Attributes.PCOLOR_MASK
|
||||
Attributes.CM_RGB:
|
||||
return fg & Attributes.RGB_MASK
|
||||
_:
|
||||
return -1 # CM_DEFAULT defaults to -1
|
||||
|
||||
|
||||
func get_bg_color() -> int:
|
||||
match bg & Attributes.CM_MASK:
|
||||
Attributes.CM_P16, Attributes.CM_P256:
|
||||
return bg & Attributes.PCOLOR_MASK
|
||||
Attributes.CM_RGB:
|
||||
return bg & Attributes.RGB_MASK
|
||||
_:
|
||||
return -1 # CM_DEFAULT defaults to -1
|
||||
|
||||
|
||||
func has_extended_attrs() -> int:
|
||||
return bg & BgFlags.HAS_EXTENDED
|
||||
|
||||
|
||||
func update_extended() -> void:
|
||||
if extended.is_empty():
|
||||
bg &= ~BgFlags.HAS_EXTENDED
|
||||
else:
|
||||
bg |= BgFlags.HAS_EXTENDED
|
||||
|
||||
|
||||
func get_underline_color() -> int:
|
||||
if bg & BgFlags.HAS_EXTENDED and ~extended.underline_color:
|
||||
match extended.underline_color & Attributes.CM_MASK:
|
||||
Attributes.CM_P16, Attributes.CM_P256:
|
||||
return extended.underline_color & Attributes.PCOLOR_MASK
|
||||
Attributes.CM_RGB:
|
||||
return extended.underline_color & Attributes.RGB_MASK
|
||||
_:
|
||||
return get_fg_color()
|
||||
else:
|
||||
return get_fg_color()
|
||||
|
||||
|
||||
func get_underline_color_mode() -> int:
|
||||
if bg & BgFlags.HAS_EXTENDED and ~extended.underline_color:
|
||||
return extended.underline_color & Attributes.CM_MASK
|
||||
else:
|
||||
return get_fg_color_mode()
|
||||
|
||||
|
||||
func is_underline_color_rgb() -> bool:
|
||||
if bg & BgFlags.HAS_EXTENDED and ~extended.underline_color:
|
||||
return extended.underline_color & Attributes.CM_MASK == Attributes.CM_RGB
|
||||
else:
|
||||
return is_fg_rgb()
|
||||
|
||||
|
||||
func is_underline_color_palette() -> bool:
|
||||
if bg & BgFlags.HAS_EXTENDED and ~extended.underline_color:
|
||||
return extended.underline_color & Attributes.CM_MASK == Attributes.CM_P16 \
|
||||
or extended.underline_color & Attributes.CM_MASK == Attributes.CM_P256
|
||||
else:
|
||||
return is_fg_palette()
|
||||
|
||||
|
||||
func is_underline_color_default() -> bool:
|
||||
if bg & BgFlags.HAS_EXTENDED and ~extended.underline_color:
|
||||
return extended.underline_color & Attributes.CM_MASK == 0
|
||||
else:
|
||||
return is_fg_default()
|
||||
|
||||
|
||||
func get_underline_style():
|
||||
if fg & FgFlags.UNDERLINE:
|
||||
return extended.underline_style if bg & BgFlags.HAS_EXTENDED else UnderlineStyle.SINGLE
|
||||
else:
|
||||
return UnderlineStyle.NONE
|
||||
|
||||
|
||||
class ExtendedAttrs:
|
||||
extends Reference
|
||||
# Extended attributes for a cell.
|
||||
# Holds information about different underline styles and color.
|
||||
|
||||
|
||||
var underline_style = UnderlineStyle.NONE
|
||||
var underline_color: int = -1
|
||||
|
||||
|
||||
func _init():
|
||||
underline_style
|
||||
|
||||
|
||||
func duplicate():
|
||||
# Workaround as per: https://github.com/godotengine/godot/issues/19345#issuecomment-471218401
|
||||
var AttributeData = load("res://addons/godot_xterm/buffer/attribute_data.gd")
|
||||
var clone = AttributeData.ExtendedAttrs.new()
|
||||
clone.underline_style = underline_style
|
||||
clone.underline_color = underline_color
|
||||
return clone
|
||||
|
||||
|
||||
# Convenient method to indicate whether the object holds no additional information,
|
||||
# that needs to be persistant in the buffer.
|
||||
func is_empty():
|
||||
return underline_style == UnderlineStyle.NONE
|
|
@ -1,430 +0,0 @@
|
|||
# Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
const BufferLine = preload("res://addons/godot_xterm/buffer/buffer_line.gd")
|
||||
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
|
||||
const Charsets = preload("res://addons/godot_xterm/data/charsets.gd")
|
||||
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
|
||||
const CircularList = preload("res://addons/godot_xterm/circular_list.gd")
|
||||
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
|
||||
const BufferReflow = preload("res://addons/godot_xterm/buffer/buffer_reflow.gd")
|
||||
|
||||
const MAX_BUFFER_SIZE = 4294967295 # 2^32 - 1
|
||||
|
||||
var lines
|
||||
var ydisp: int = 0
|
||||
var ybase: int = 0
|
||||
var y: int = 0
|
||||
var x: int = 0
|
||||
var scroll_bottom: int
|
||||
var scroll_top: int
|
||||
var tabs = {}
|
||||
var saved_y: int = 0
|
||||
var saved_x: int = 0
|
||||
var saved_cur_attr_data = AttributeData.new()
|
||||
var saved_charset = Charsets.DEFAULT_CHARSET
|
||||
var markers: Array = []
|
||||
|
||||
var _null_cell = CellData.from_char_data([0, Constants.NULL_CELL_CHAR,
|
||||
Constants.NULL_CELL_WIDTH, Constants.NULL_CELL_CODE])
|
||||
var _whitespace_cell = CellData.from_char_data([0, Constants.WHITESPACE_CELL_CHAR,
|
||||
Constants.WHITESPACE_CELL_WIDTH, Constants.WHITESPACE_CELL_CODE])
|
||||
var _cols: int
|
||||
var _rows: int
|
||||
var _has_scrollback
|
||||
var _options_service
|
||||
var _buffer_service
|
||||
|
||||
|
||||
func _init(has_scrollback: bool, options_service, buffer_service):
|
||||
_has_scrollback = has_scrollback
|
||||
_options_service = options_service
|
||||
_buffer_service = buffer_service
|
||||
_cols = buffer_service.cols
|
||||
_rows = buffer_service.rows
|
||||
lines = CircularList.new(_get_correct_buffer_length(_rows))
|
||||
scroll_top = 0
|
||||
scroll_bottom = _rows - 1
|
||||
setup_tab_stops()
|
||||
|
||||
|
||||
# Resizes the buffer, adjusting its data accordingly.
|
||||
# @param new_cols The new number of columns.
|
||||
# @param new_rows The new number of rows.
|
||||
func resize(new_cols: int, new_rows: int) -> void:
|
||||
# store reference to null cell with default attrs
|
||||
var null_cell = get_null_cell(AttributeData.new())
|
||||
|
||||
# Increase max length if needed before adjustments to allow space to fill
|
||||
# as required.
|
||||
var new_max_length = _get_correct_buffer_length(new_rows)
|
||||
if new_max_length > lines.max_length:
|
||||
lines.max_length = new_max_length
|
||||
|
||||
# The following adjustments should only happen if the buffer has been
|
||||
# initialized/filled.
|
||||
if lines.length > 0:
|
||||
# Deal with columns increasing (reducing needs to happen after reflow)
|
||||
if _cols < new_cols:
|
||||
for i in range(lines.length):
|
||||
lines.get_line(i).resize(new_cols, null_cell)
|
||||
|
||||
# Resize rows in both directions as needed
|
||||
var add_to_y = 0
|
||||
if _rows < new_rows:
|
||||
for y in range(_rows, new_rows):
|
||||
if lines.length < new_rows + ybase:
|
||||
if _options_service.options.windows_mode:
|
||||
# Just add the new missing rows on Windows as conpty reprints the screen with it's
|
||||
# view of the world. Once a line enters scrollback for conpty it remains there
|
||||
lines.push(BufferLine.new(new_cols, null_cell))
|
||||
else:
|
||||
if ybase > 0 and lines.length <= ybase + y + add_to_y + 1:
|
||||
# There is room above the buffer and there are no empty elements below the line,
|
||||
# scroll up
|
||||
ybase -= 1
|
||||
add_to_y += 1
|
||||
if ydisp > 0:
|
||||
# Viewport is at the top of the buffer, must increase downwards
|
||||
ydisp -= 1
|
||||
else:
|
||||
# Add a blank line if tere is no buffer left at the top to srcoll to, or if there
|
||||
# are blank lines after the cursor
|
||||
lines.push(BufferLine.new(new_cols, null_cell))
|
||||
else: # _rows >= new_rows
|
||||
for _y in range(_rows, new_rows, -1):
|
||||
if lines.length > new_rows + ybase:
|
||||
if lines.length > ybase + y + 1:
|
||||
# The line is blank line below the cursor, remove it
|
||||
lines.pop()
|
||||
else:
|
||||
# The line is the cursor, scroll down
|
||||
ybase += 1
|
||||
ydisp += 1
|
||||
|
||||
# Reduce max length if needed after adjustments, this is done after as it
|
||||
# would otherwise cut data from the bottom of the buffer.
|
||||
if new_max_length < lines.max_length:
|
||||
# Trim from the top of th ebuffer and adjust ybase and ydisp.
|
||||
var amount_to_trim = lines.length - new_max_length
|
||||
if amount_to_trim > 0:
|
||||
lines.trim_start(amount_to_trim)
|
||||
ybase = max(ybase - amount_to_trim, 0)
|
||||
ydisp = max(ydisp - amount_to_trim, 0)
|
||||
saved_y = max(saved_y - amount_to_trim, 0)
|
||||
lines.max_length = new_max_length
|
||||
|
||||
# Make sure that the cursor stays on screen
|
||||
x = min(x, new_cols - 1)
|
||||
y = min(y, new_rows - 1)
|
||||
if add_to_y:
|
||||
y += add_to_y
|
||||
saved_x = min(saved_x, new_cols -1)
|
||||
|
||||
scroll_top = 0
|
||||
|
||||
scroll_bottom = new_rows - 1
|
||||
|
||||
if _is_reflow_enabled():
|
||||
_reflow(new_cols, new_rows)
|
||||
|
||||
# Trim the end of the line off if cols shrunk
|
||||
if _cols > new_cols:
|
||||
for i in range(lines.length):
|
||||
lines.get_line(i).resize(new_cols, null_cell)
|
||||
|
||||
_cols = new_cols
|
||||
_rows = new_rows
|
||||
|
||||
|
||||
func _is_reflow_enabled() -> bool:
|
||||
return _has_scrollback and not _options_service.options.windows_mode
|
||||
|
||||
|
||||
func _reflow(new_cols: int, new_rows: int) -> void:
|
||||
if _cols == new_cols:
|
||||
return
|
||||
|
||||
# Iterate through rows, ignore the last one as it cannot be wrapped
|
||||
if new_cols > _cols:
|
||||
_reflow_larger(new_cols, new_rows)
|
||||
else:
|
||||
_reflow_smaller(new_cols, new_rows)
|
||||
|
||||
|
||||
func _reflow_larger(new_cols: int, new_rows: int) -> void:
|
||||
var to_remove: PoolIntArray = BufferReflow.reflow_larger_get_lines_to_remove(lines,
|
||||
_cols, new_cols, ybase + y, get_null_cell(AttributeData.new()))
|
||||
if not to_remove.empty():
|
||||
var new_layout_result = BufferReflow.reflow_larger_create_new_layout(lines, to_remove)
|
||||
BufferReflow.reflow_larger_apply_new_layout(lines, new_layout_result.layout)
|
||||
_reflow_larger_adjust_viewport(new_cols, new_rows, new_layout_result.count_removed)
|
||||
|
||||
|
||||
func _reflow_larger_adjust_viewport(new_cols: int, new_rows: int, count_removed: int) -> void:
|
||||
var null_cell = get_null_cell(AttributeData.new())
|
||||
# Adjust viewport based on number of items removed
|
||||
var viewport_adjustments = count_removed
|
||||
while viewport_adjustments > 0:
|
||||
viewport_adjustments -= 1
|
||||
if ybase == 0:
|
||||
if y > 0:
|
||||
y -= 1
|
||||
if lines.length < new_rows:
|
||||
# Add an extra row at the bottom of the viewport
|
||||
lines.push(BufferLine.new(new_cols, null_cell))
|
||||
else:
|
||||
if ydisp == ybase:
|
||||
ydisp -= 1
|
||||
ybase -= 1
|
||||
|
||||
saved_y = max(saved_y - count_removed, 0)
|
||||
|
||||
|
||||
func _reflow_smaller(new_cols: int, new_rows: int) -> void:
|
||||
var null_cell = get_null_cell(AttributeData.new())
|
||||
# Gather all BufferLines that need to be inserted into the Buffer here so that they can be
|
||||
# batched up and only commited once
|
||||
var to_insert = []
|
||||
var count_to_insert = 0
|
||||
# Go backwards as many lines may be trimmed and this will avoid considering them
|
||||
var i = lines.length - 1
|
||||
while i >= 0:
|
||||
# Check wether this line is a problem
|
||||
var next_line = lines.get_line(i)
|
||||
if not next_line or not next_line.is_wrapped and next_line.get_trimmed_length() <= new_cols:
|
||||
i -= 1
|
||||
continue
|
||||
|
||||
# Gather wrapped lines and adjust y to be the starting line
|
||||
var wrapped_lines = [next_line]
|
||||
while next_line.is_wrapped and i > 0:
|
||||
i -= 1
|
||||
next_line = lines.get_line(i)
|
||||
wrapped_lines.push_front(next_line)
|
||||
|
||||
# If these lines contain the cursor don't touch them, the program will handle fixing up
|
||||
# wrapped lines with the cursor
|
||||
var absolute_y = ybase + y
|
||||
if absolute_y >= i and absolute_y < i + wrapped_lines.size():
|
||||
i -= 1
|
||||
continue
|
||||
|
||||
var last_line_length = wrapped_lines[wrapped_lines.size() - 1].get_trimmed_length()
|
||||
var dest_line_lengths = BufferReflow.reflow_smaller_get_new_line_lengths(wrapped_lines, _cols, new_cols)
|
||||
var lines_to_add = dest_line_lengths.size() - wrapped_lines.size()
|
||||
var trimmed_lines: int
|
||||
if ybase == 0 and y != lines.length - 1:
|
||||
# If the top section of the buffer is not yet filled
|
||||
trimmed_lines = max(0, y - lines.max_length + lines_to_add)
|
||||
else:
|
||||
trimmed_lines = max(0, lines.length - lines.max_length + lines_to_add)
|
||||
|
||||
# Add the new lines
|
||||
var new_lines = []
|
||||
for j in range(lines_to_add):
|
||||
var new_line = get_blank_line(AttributeData.new(), true)
|
||||
new_lines.append(new_line)
|
||||
if not new_lines.empty():
|
||||
to_insert.append({"start": i + wrapped_lines.size() + count_to_insert,
|
||||
"new_lines": new_lines})
|
||||
count_to_insert += new_lines.size()
|
||||
wrapped_lines += new_lines
|
||||
|
||||
# Copy buffer data to new locations, this needs to happen backwards to do in-place
|
||||
var dest_line_index = dest_line_lengths.size() - 1 # floor(cells_needed / new_cols)
|
||||
var dest_col = dest_line_lengths[dest_line_index] # cells_needed % new_cols
|
||||
if dest_col == 0:
|
||||
dest_line_index -= 1
|
||||
dest_col = dest_line_lengths[dest_line_index]
|
||||
var src_line_index = wrapped_lines.size() - lines_to_add - 1
|
||||
var src_col = last_line_length
|
||||
while src_line_index >= 0:
|
||||
var cells_to_copy = min(src_col, dest_col)
|
||||
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[src_line_index],
|
||||
src_col - cells_to_copy, dest_col - cells_to_copy, cells_to_copy, true)
|
||||
dest_col -= cells_to_copy
|
||||
if dest_col == 0:
|
||||
dest_line_index -= 1
|
||||
dest_col = dest_line_lengths[dest_line_index]
|
||||
src_col -= cells_to_copy
|
||||
if src_col == 0:
|
||||
src_line_index -= 1
|
||||
var wrapped_lines_index = max(src_line_index, 0)
|
||||
src_col = BufferReflow.get_wrapped_line_trimmed_length(wrapped_lines, wrapped_lines_index, _cols)
|
||||
|
||||
# Null out the end of the line ends if a wide character wrapped to the following line
|
||||
for j in range(wrapped_lines.size()):
|
||||
if dest_line_lengths[j] < new_cols:
|
||||
wrapped_lines[j].set_cell(dest_line_lengths[j], null_cell)
|
||||
|
||||
# Adjust viewport as needed
|
||||
var viewport_adjustments = lines_to_add - trimmed_lines
|
||||
while viewport_adjustments > 0:
|
||||
if ybase == 0:
|
||||
if y < new_rows - 1:
|
||||
y += 1
|
||||
lines.pop()
|
||||
else:
|
||||
ybase += 1
|
||||
ydisp += 1
|
||||
else:
|
||||
# Ensure ybase does not exceed its maximum value
|
||||
if ybase < min(lines.max_length, (lines.length + count_to_insert) - new_rows):
|
||||
if ybase == ydisp:
|
||||
ydisp += 1
|
||||
ybase += 1
|
||||
viewport_adjustments -= 1
|
||||
saved_y = min(saved_y + lines_to_add, ybase + new_rows - 1)
|
||||
i -= 1
|
||||
|
||||
# Rearrange lines in the buffer if there are any insertions, this is done at the end rather
|
||||
# than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
|
||||
# costly calls to CircularList.splice.
|
||||
if not to_insert.empty():
|
||||
# Record buffer insert events and then play them backwards so that the indexes are
|
||||
# correct
|
||||
var insert_events = []
|
||||
|
||||
# Record original lines so they don't get overridden when we rearrange the list
|
||||
var original_lines = []
|
||||
for j in range(lines.length):
|
||||
original_lines.append(lines.get_line(j))
|
||||
var original_lines_length = lines.length
|
||||
|
||||
var original_line_index = original_lines_length - 1
|
||||
var next_to_insert_index = 0
|
||||
var next_to_insert = to_insert[next_to_insert_index]
|
||||
lines.length = min(lines.max_length, lines.length + count_to_insert)
|
||||
var count_inserted_so_far = 0
|
||||
var j = min(lines.max_length - 1, original_lines_length + count_to_insert - 1)
|
||||
while j >= 0:
|
||||
if next_to_insert and next_to_insert.start > original_line_index + count_inserted_so_far:
|
||||
# Insert extra lines here, adjusting i as needed
|
||||
for next_i in range(next_to_insert.new_lines.size() - 1, -1, -1):
|
||||
lines.set_line(j, next_to_insert.new_lines[next_i])
|
||||
j -= 1
|
||||
j += 1
|
||||
|
||||
# Create insert events for later
|
||||
insert_events.append({"index": original_line_index + 1,
|
||||
"amount": next_to_insert.new_lines.size()})
|
||||
count_inserted_so_far += next_to_insert.new_lines.size()
|
||||
next_to_insert_index += 1
|
||||
next_to_insert = to_insert[next_to_insert_index] if to_insert.size() > next_to_insert_index else null
|
||||
else:
|
||||
lines.set_line(j, original_lines[original_line_index])
|
||||
original_line_index -= 1
|
||||
j -= 1
|
||||
|
||||
# Update markers
|
||||
var insert_count_emitted = 0
|
||||
for k in range(insert_events.size() - 1, -1, -1):
|
||||
insert_events[k].index += insert_count_emitted
|
||||
lines.emit_signal("inserted", insert_events[k])
|
||||
insert_count_emitted += insert_events[k].amount
|
||||
var amount_to_trim = max(0, original_lines_length + count_to_insert - lines.max_length)
|
||||
if amount_to_trim > 0:
|
||||
lines.emit_signal("trimmed", amount_to_trim)
|
||||
|
||||
|
||||
func get_null_cell(attr = null):
|
||||
if attr:
|
||||
_null_cell.fg = attr.fg
|
||||
_null_cell.bg = attr.bg
|
||||
_null_cell.extended = attr.extended
|
||||
else:
|
||||
_null_cell.fg = 0
|
||||
_null_cell.bg = 0
|
||||
_null_cell.extended = AttributeData.ExtendedAttrs.new()
|
||||
return _null_cell
|
||||
|
||||
|
||||
func get_blank_line(attr, is_wrapped: bool = false):
|
||||
return BufferLine.new(_buffer_service.cols, get_null_cell(attr), is_wrapped)
|
||||
|
||||
|
||||
func _get_correct_buffer_length(rows: int) -> int:
|
||||
if not _has_scrollback:
|
||||
return rows
|
||||
else:
|
||||
var correct_buffer_length = rows + _options_service.options.scrollback
|
||||
return correct_buffer_length if correct_buffer_length < MAX_BUFFER_SIZE else MAX_BUFFER_SIZE
|
||||
|
||||
|
||||
# Fills the viewport with blank lines.
|
||||
func fill_viewport_rows(fill_attr = null) -> void:
|
||||
if lines.length == 0:
|
||||
if not fill_attr:
|
||||
fill_attr = AttributeData.new()
|
||||
var i = _rows
|
||||
while i:
|
||||
lines.push(get_blank_line(fill_attr))
|
||||
i -= 1
|
||||
|
||||
|
||||
|
||||
# Clears the buffer to it's initial state, discarding all previous data.
|
||||
func clear() -> void:
|
||||
ydisp = 0
|
||||
ybase = 0
|
||||
y = 0
|
||||
x = 0
|
||||
lines = CircularList.new(_get_correct_buffer_length(_rows))
|
||||
scroll_top = 0
|
||||
scroll_bottom = _rows - 1
|
||||
setup_tab_stops()
|
||||
|
||||
|
||||
func get_wrapped_range_for_line(y: int) -> Dictionary:
|
||||
var first = y
|
||||
var last = y
|
||||
# Scan upwards for wrapped lines
|
||||
while first > 0 and lines.get_el(first).is_wrapped:
|
||||
first -= 1
|
||||
# Scan downwards for wrapped lines
|
||||
while last + 1 < lines.length and lines.get_el(last + 1).is_wrapped:
|
||||
last += 1
|
||||
return {"first": first, "last": last}
|
||||
|
||||
|
||||
# Setup the tab stops.
|
||||
# @param i The index to start setting up tab stops from.
|
||||
func setup_tab_stops(i = null) -> void:
|
||||
if i != null:
|
||||
if not tabs.get(i):
|
||||
i = prev_stop(i)
|
||||
else:
|
||||
tabs = {}
|
||||
i = 0
|
||||
|
||||
while i < _cols:
|
||||
tabs[i] = true
|
||||
i += max(_options_service.options.tab_stop_width, 1)
|
||||
|
||||
|
||||
# Move the cursor to the previous tab stop from the given position (default is current).
|
||||
# @param x The position to move the cursor to the previous tab stop.
|
||||
func prev_stop(x: int) -> int:
|
||||
if x == null:
|
||||
x = self.x
|
||||
|
||||
while not tabs.get(x - 1, false) and x - 1 > 0:
|
||||
x - 1
|
||||
|
||||
return _cols - 1 if x > _cols else 0 if x < 0 else x
|
||||
|
||||
# Move the cursor one tab stop forward from the given position (default is current).
|
||||
# @param x The position to move the cursor one tab stop forward.
|
||||
func next_stop(x = null) -> int:
|
||||
if x == null:
|
||||
x = self.x
|
||||
|
||||
x += 1
|
||||
while not tabs.get(x) and x < _cols:
|
||||
x += 1
|
||||
|
||||
return _cols - 1 if x >= _cols else 0 if x < 0 else x
|
|
@ -1,291 +0,0 @@
|
|||
# Copyright (c) 2018 The xterm.js authors. All rights reserved
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
|
||||
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
|
||||
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
|
||||
const Content = Constants.Content
|
||||
const BgFlags = Constants.BgFlags
|
||||
|
||||
const CELL_SIZE = 3
|
||||
|
||||
enum Cell {
|
||||
CONTENT
|
||||
FG
|
||||
BG
|
||||
}
|
||||
|
||||
var _data: Array
|
||||
var _combined: Dictionary = {}
|
||||
var _extended_attrs: Dictionary = {}
|
||||
|
||||
var length: int
|
||||
var is_wrapped
|
||||
|
||||
func _init(cols: int, fill_cell_data = null, is_wrapped: bool = false):
|
||||
self.is_wrapped = is_wrapped
|
||||
_data = []
|
||||
_data.resize(cols * CELL_SIZE)
|
||||
var cell = fill_cell_data if fill_cell_data \
|
||||
else CellData.from_char_data([0, Constants.NULL_CELL_CHAR, Constants.NULL_CELL_WIDTH, Constants.NULL_CELL_CODE])
|
||||
for i in range(cols):
|
||||
set_cell(i, cell)
|
||||
length = cols
|
||||
|
||||
|
||||
func get_cell(index: int):
|
||||
return _data[index * CELL_SIZE + Cell.CONTENT]
|
||||
|
||||
|
||||
func get_width(index: int) -> int:
|
||||
if (index * CELL_SIZE + Cell.CONTENT) < _data.size():
|
||||
return _data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
func has_content(index: int) -> int:
|
||||
return _data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK
|
||||
|
||||
|
||||
# Get codepoint of the cell.
|
||||
# To be in line with `code` in CharData this either returns
|
||||
# a single UTF32 codepoint or the last codepoint of a combined string.
|
||||
func get_codepoint(index: int) -> int:
|
||||
var content = _data[index * CELL_SIZE + Cell.CONTENT]
|
||||
if content & Content.IS_COMBINED_MASK:
|
||||
return _combined[index].ord_at(_combined[index].length() - 1)
|
||||
else:
|
||||
return content & Content.CODEPOINT_MASK
|
||||
|
||||
|
||||
func load_cell(index: int, cell):
|
||||
var start_index = index * CELL_SIZE
|
||||
cell.content = _data[start_index + Cell.CONTENT]
|
||||
cell.fg = _data[start_index + Cell.FG]
|
||||
cell.bg = _data[start_index + Cell.BG]
|
||||
if cell.content and cell.content & Content.IS_COMBINED_MASK:
|
||||
cell.combined_data = _combined[index]
|
||||
if cell.bg & BgFlags.HAS_EXTENDED:
|
||||
cell.extended = _extended_attrs[index]
|
||||
return cell
|
||||
|
||||
|
||||
func set_cell(index: int, cell) -> void:
|
||||
if cell.content & Content.IS_COMBINED_MASK:
|
||||
_combined[index] = cell.combined_data
|
||||
if cell.bg & BgFlags.HAS_EXTENDED:
|
||||
_extended_attrs[index] = cell.extended
|
||||
_data[index * CELL_SIZE + Cell.CONTENT] = cell.content
|
||||
_data[index * CELL_SIZE + Cell.FG] = cell.fg
|
||||
_data[index * CELL_SIZE + Cell.BG] = cell.bg
|
||||
|
||||
|
||||
func set_cell_from_codepoint(index: int, codepoint: int, width: int, fg: int, bg: int, e_attrs) -> void:
|
||||
if bg & BgFlags.HAS_EXTENDED:
|
||||
_extended_attrs[index] = e_attrs
|
||||
_data[index * CELL_SIZE + Cell.CONTENT] = codepoint | (width << Content.WIDTH_SHIFT)
|
||||
_data[index * CELL_SIZE + Cell.FG] = fg
|
||||
_data[index * CELL_SIZE + Cell.BG] = bg
|
||||
|
||||
|
||||
# Add a codepoint to a cell from input handler
|
||||
# During input stage combining chars with a width of 0 follow and stack
|
||||
# onto a leading char. Since we already set the attrs
|
||||
# by the previous `set_data_from_code_pont` call, we can omit it here.
|
||||
func add_codepoint_to_cell(index: int, codepoint: int) -> void:
|
||||
var content = _data[index * CELL_SIZE + Cell.CONTENT]
|
||||
if content & Content.IS_COMBINED_MASK:
|
||||
# we already have a combined string, simply add
|
||||
_combined[index] += char(codepoint)
|
||||
else:
|
||||
if content & Content.CODEPOINT_MASK:
|
||||
# normal case for combining chars:
|
||||
# - move current leading char + new one into combined string
|
||||
# - set combined flag
|
||||
_combined[index] = char(content & Content.CODEPOINT_MASK) + char(codepoint)
|
||||
content &= ~Content.CODEPOINT_MASK # set codepoint in buffer to 0
|
||||
content |= Content.IS_COMBINED_MASK
|
||||
else:
|
||||
# should not happen - we actually have no data in the cell yet
|
||||
# simply set the data in the cell buffer with a width of 1
|
||||
content = codepoint | (1 << Content.WIDTH_SHIFT)
|
||||
_data[index * CELL_SIZE + Cell.CONTENT] = content
|
||||
|
||||
|
||||
func insert_cells(pos: int, n: int, fill_cell_data, erase_attr = null) -> void:
|
||||
pos %= length
|
||||
|
||||
# handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char
|
||||
var fg = erase_attr.fg if erase_attr and erase_attr.fg else 0
|
||||
var bg = erase_attr.bg if erase_attr and erase_attr.bg else 0
|
||||
var extended = erase_attr.extended if erase_attr and erase_attr.extended else AttributeData.ExtendedAttrs.new()
|
||||
if pos and get_width(pos - 1) == 2:
|
||||
set_cell_from_codepoint(pos - 1, 0, 1, fg, bg, extended)
|
||||
|
||||
if n < length - pos:
|
||||
var cell = CellData.new()
|
||||
var i = length - pos - n - 1
|
||||
while i >= 0:
|
||||
set_cell(pos + n + i, load_cell(pos + i, cell))
|
||||
i -= 1
|
||||
for j in range(n):
|
||||
set_cell(pos + j, fill_cell_data)
|
||||
else:
|
||||
for i in range(pos, length):
|
||||
set_cell(i, fill_cell_data)
|
||||
|
||||
# handle fullwidth at line end: reset last cell if it is first cell of a wide char
|
||||
if get_width(length - 1) == 2:
|
||||
set_cell_from_codepoint(length - 1, 0, 1, fg, bg, extended)
|
||||
|
||||
|
||||
func delete_cells(pos: int, n: int, fill_cell_data, erase_attr = null) -> void:
|
||||
pos %= length
|
||||
if n < length - pos:
|
||||
var cell = CellData.new()
|
||||
for i in range(length - pos - n):
|
||||
set_cell(pos + i, load_cell(pos + n + i, cell))
|
||||
for i in range(length - n, length):
|
||||
set_cell(i, fill_cell_data)
|
||||
else:
|
||||
for i in range(pos, length):
|
||||
set_cell(i, fill_cell_data)
|
||||
|
||||
# handle fullwidth at pos:
|
||||
# - reset pos-1 if wide char
|
||||
# - reset pos if width==0 (previous second cell of a wide char)
|
||||
var fg = erase_attr.fg if erase_attr and erase_attr.fg else 0
|
||||
var bg = erase_attr.bg if erase_attr and erase_attr.bg else 0
|
||||
var extended = erase_attr.extended if erase_attr and erase_attr.extended else AttributeData.ExtendedAttrs.new()
|
||||
if pos and get_width(pos - 1) == 2:
|
||||
set_cell_from_codepoint(pos - 1, 0, 1, fg, bg, extended)
|
||||
if get_width(pos) == 0 and not has_content(pos):
|
||||
set_cell_from_codepoint(pos, 0, 1, fg, bg, extended)
|
||||
|
||||
|
||||
func replace_cells(start: int, end: int, fill_cell_data, erase_attr = null) -> void:
|
||||
var fg = erase_attr.fg if erase_attr and erase_attr.fg else 0
|
||||
var bg = erase_attr.bg if erase_attr and erase_attr.bg else 0
|
||||
var extended = erase_attr.extended if erase_attr and erase_attr.extended else AttributeData.ExtendedAttrs.new()
|
||||
|
||||
# handle fullwidth at start: reset cell one to left if start is second cell of a wide char
|
||||
if start and get_width(start - 1) == 2:
|
||||
set_cell_from_codepoint(start - 1, 0, 1, fg, bg, extended)
|
||||
# handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char
|
||||
if end < length and get_width(end - 1) == 2:
|
||||
set_cell_from_codepoint(end, 0, 1, fg, bg, extended)
|
||||
|
||||
while start < end and start < length:
|
||||
set_cell(start, fill_cell_data)
|
||||
start += 1
|
||||
|
||||
|
||||
func resize(cols: int, fill_cell_data) -> void:
|
||||
if cols == length:
|
||||
return
|
||||
if cols > length:
|
||||
var data = []
|
||||
if length:
|
||||
if cols * CELL_SIZE < _data.size():
|
||||
data = _data.slice(0, cols * CELL_SIZE - 1)
|
||||
else:
|
||||
data = _data.duplicate()
|
||||
data.resize(cols * CELL_SIZE)
|
||||
_data = data
|
||||
var i = length
|
||||
while i < cols:
|
||||
set_cell(i, fill_cell_data)
|
||||
i += 1
|
||||
else:
|
||||
if cols:
|
||||
var data = []
|
||||
data = _data.slice(0, cols * CELL_SIZE - 1)
|
||||
data.resize(cols * CELL_SIZE)
|
||||
_data = data
|
||||
# Remove any cut off combined data, FIXME: repeat this for extended attrs
|
||||
for key in _combined.keys():
|
||||
if key as int > cols:
|
||||
_combined.erase(key)
|
||||
else:
|
||||
_data = []
|
||||
_combined = {}
|
||||
length = cols
|
||||
|
||||
|
||||
# Fill a line with `fill_cell_data`.
|
||||
func fill(fill_cell_data) -> void:
|
||||
_combined = {}
|
||||
_extended_attrs = {}
|
||||
for i in range(length):
|
||||
set_cell(i, fill_cell_data)
|
||||
|
||||
|
||||
# alter to a full copy of line
|
||||
func copy_from(line) -> void:
|
||||
_data = line._data.duplicate()
|
||||
length = line.length
|
||||
_combined = {}
|
||||
for k in line._combined.keys():
|
||||
_combined[k] = line._combined[k]
|
||||
is_wrapped = line.is_wrapped
|
||||
|
||||
|
||||
func get_trimmed_length() -> int:
|
||||
for i in range(length - 1, -1, -1):
|
||||
if _data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK:
|
||||
return i + (_data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT)
|
||||
return 0
|
||||
|
||||
|
||||
func copy_cells_from(src, src_col: int, dest_col: int, length: int, apply_in_reverse: bool) -> void:
|
||||
var src_data = src._data
|
||||
|
||||
if apply_in_reverse:
|
||||
for cell in range(length - 1, -1, -1):
|
||||
for i in range(CELL_SIZE):
|
||||
_data[(dest_col + cell) * CELL_SIZE + i] = src_data[(src_col + cell) * CELL_SIZE + i]
|
||||
else:
|
||||
for cell in range(length):
|
||||
for i in range(CELL_SIZE):
|
||||
_data[(dest_col + cell) * CELL_SIZE + i] = src_data[(src_col + cell) * CELL_SIZE + i]
|
||||
|
||||
# Move any combined data over as needed, FIXME: repeat for extended attrs
|
||||
var src_combined_keys = src._combined.keys()
|
||||
for i in range(src_combined_keys.size()):
|
||||
var key = int(src_combined_keys[i])
|
||||
if key >= src_col:
|
||||
_combined[key + src_col + dest_col] = src._combined[key]
|
||||
|
||||
|
||||
func translate_to_string(trim_right: bool = false, start_col: int = 0, end_col: int = -1) -> String:
|
||||
if end_col == -1:
|
||||
end_col = length
|
||||
if trim_right:
|
||||
end_col = min(end_col, get_trimmed_length())
|
||||
var result = ""
|
||||
while start_col < end_col:
|
||||
var content = _data[start_col * CELL_SIZE + Cell.CONTENT]
|
||||
var cp = content & Content.CODEPOINT_MASK
|
||||
if content & Content.IS_COMBINED_MASK:
|
||||
result += _combined[start_col]
|
||||
elif cp:
|
||||
result += char(cp)
|
||||
else:
|
||||
result += Constants.WHITESPACE_CELL_CHAR
|
||||
start_col += max(content >> Content.WIDTH_SHIFT, 1) # always advance by 1
|
||||
return result
|
||||
|
||||
|
||||
func duplicate():
|
||||
# Workaround as per: https://github.com/godotengine/godot/issues/19345#issuecomment-471218401
|
||||
var duplicant = load("res://addons/godot_xterm/buffer/buffer_line.gd").new(length)
|
||||
duplicant._data = _data.duplicate(true)
|
||||
duplicant._combined = _combined.duplicate(true)
|
||||
duplicant._extended_attrs = _extended_attrs.duplicate(true)
|
||||
duplicant.length = length
|
||||
duplicant.is_wrapped = is_wrapped
|
||||
return duplicant
|
|
@ -1,198 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Object
|
||||
|
||||
|
||||
# Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed
|
||||
# when a wrapped line unwraps.
|
||||
# @param lines The buffer lines.
|
||||
# @param newCols The columns after resize.
|
||||
static func reflow_larger_get_lines_to_remove(lines, old_cols: int, new_cols: int,
|
||||
buffer_absolute_y: int, null_cell) -> PoolIntArray:
|
||||
# Gather all BufferLines that need to be removed from the Buffer here so that they can be
|
||||
# batched up and only committed once
|
||||
var to_remove = PoolIntArray([])
|
||||
|
||||
var y = 0
|
||||
while y < lines.length - 1:
|
||||
# Check if this row is wrapped
|
||||
var i = y
|
||||
i += 1
|
||||
var next_line = lines.get_line(i)
|
||||
if not next_line.is_wrapped:
|
||||
y += 1
|
||||
continue
|
||||
|
||||
# Check how many lines it's wrapped for
|
||||
var wrapped_lines = [lines.get_line(y)]
|
||||
while i < lines.length and next_line.is_wrapped:
|
||||
wrapped_lines.append(next_line)
|
||||
i += 1
|
||||
next_line = lines.get_line(i)
|
||||
|
||||
# If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
|
||||
# lines with the cursor
|
||||
if buffer_absolute_y >= y and buffer_absolute_y < i:
|
||||
y += wrapped_lines.size() - 1
|
||||
y += 1
|
||||
continue
|
||||
|
||||
# Copy buffer data to new locations
|
||||
var dest_line_index = 0
|
||||
var dest_col = get_wrapped_line_trimmed_length(wrapped_lines, dest_line_index, old_cols)
|
||||
var src_line_index = 1
|
||||
var src_col = 0
|
||||
while src_line_index < wrapped_lines.size():
|
||||
var src_trimmed_line_length = get_wrapped_line_trimmed_length(wrapped_lines, src_line_index, old_cols)
|
||||
var src_remaining_cells = src_trimmed_line_length - src_col
|
||||
var dest_remaining_cells = new_cols - dest_col
|
||||
var cells_to_copy = min(src_remaining_cells, dest_remaining_cells)
|
||||
|
||||
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[src_line_index], src_col, dest_col, cells_to_copy, false)
|
||||
|
||||
dest_col += cells_to_copy
|
||||
if dest_col == new_cols:
|
||||
dest_line_index += 1
|
||||
dest_col = 0
|
||||
|
||||
src_col += cells_to_copy
|
||||
if src_col == src_trimmed_line_length:
|
||||
src_line_index += 1
|
||||
src_col = 0
|
||||
|
||||
# Make sure the last cell isn't wide, if it is copy it to the current dest
|
||||
if dest_col == 0 and dest_line_index != 0:
|
||||
if wrapped_lines[dest_line_index - 1].get_width(new_cols - 1) == 2:
|
||||
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[dest_line_index - 1], new_cols - 1, dest_col, 1, false)
|
||||
dest_col += 1
|
||||
# Null out the end of the last row
|
||||
wrapped_lines[dest_line_index - 1].set_cell(new_cols - 1, null_cell)
|
||||
|
||||
# Clear out remaining cells or fragments could remain;
|
||||
var replaced = wrapped_lines[dest_line_index].translate_to_string()
|
||||
wrapped_lines[dest_line_index].replace_cells(dest_col, new_cols, null_cell)
|
||||
|
||||
# Work backwards and remove any rows at the end that only contain null cells
|
||||
var count_to_remove = 0
|
||||
for j in range(wrapped_lines.size() - 1, 0, -1):
|
||||
if j > dest_line_index or wrapped_lines[j].get_trimmed_length() == 0:
|
||||
count_to_remove += 1
|
||||
else:
|
||||
break
|
||||
|
||||
if count_to_remove > 0:
|
||||
to_remove.append(y + wrapped_lines.size() - count_to_remove) # index
|
||||
to_remove.append(count_to_remove)
|
||||
|
||||
y += wrapped_lines.size() - 1
|
||||
y += 1
|
||||
|
||||
return to_remove
|
||||
|
||||
|
||||
# Creates and return the new layout for lines given an array of indexes to be removed.
|
||||
# @param lines The buffer lines.
|
||||
# @param to_remove The indexes to remove.
|
||||
static func reflow_larger_create_new_layout(lines, to_remove: PoolIntArray):
|
||||
var layout = PoolIntArray([])
|
||||
# First iterate through the list and get the actual indexes to use for rows
|
||||
var next_to_remove_index = 0
|
||||
var next_to_remove_start = to_remove[next_to_remove_index]
|
||||
var count_removed_so_far = 0
|
||||
var i = 0
|
||||
while i < lines.length:
|
||||
if next_to_remove_start == i:
|
||||
next_to_remove_index += 1
|
||||
var count_to_remove = to_remove[next_to_remove_index]
|
||||
|
||||
# Tell markers that there was a deletion
|
||||
lines.emit_signal("deleted", i - count_removed_so_far, count_to_remove)
|
||||
|
||||
i += count_to_remove - 1
|
||||
count_removed_so_far += count_to_remove
|
||||
next_to_remove_index += 1
|
||||
next_to_remove_start = to_remove[next_to_remove_index] if next_to_remove_index < to_remove.size() else null
|
||||
else:
|
||||
layout.append(i)
|
||||
|
||||
i += 1
|
||||
|
||||
return { "layout": layout, "count_removed": count_removed_so_far }
|
||||
|
||||
|
||||
# Applies a new layout to the buffer. This essentially does the same as many splice calls but it's
|
||||
# done all at once in a single iteration through the list since splice is very expensive.
|
||||
# @param lines The buffer lines.
|
||||
# @param new_layout The new layout to apply.
|
||||
static func reflow_larger_apply_new_layout(lines, new_layout: PoolIntArray) -> void:
|
||||
# Record original lines so they don't get overridden when we rearrange the list
|
||||
var new_layout_lines = []
|
||||
for i in range(new_layout.size()):
|
||||
new_layout_lines.append(lines.get_line(new_layout[i]))
|
||||
|
||||
# Rearrange the list
|
||||
for i in range(new_layout_lines.size()):
|
||||
lines.set_line(i, new_layout_lines[i])
|
||||
lines.length = new_layout.size()
|
||||
|
||||
|
||||
# Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
|
||||
# compute the wrapping points since wide characters may need to be wrapped onto the following line.
|
||||
# This function will return an array of numbers of where each line wraps to, the resulting array
|
||||
# will only contain the values `newCols` (when the line does not end with a wide character) and
|
||||
# `new_cols - 1` (when the line does end with a wide character), except for the last value which
|
||||
# will contain the remaining items to fill the line.
|
||||
#
|
||||
# Calling this with a `new_cols` value of `1` will lock up.
|
||||
#
|
||||
# @param wrapped_lines The wrapped lines to evaluate.
|
||||
# @param old_cols The columns before resize.
|
||||
# @param new_cols The columns after resize.
|
||||
static func reflow_smaller_get_new_line_lengths(wrapped_lines: Array, old_cols: int, new_cols: int) -> PoolIntArray:
|
||||
var new_line_lengths = PoolIntArray([])
|
||||
var cells_needed: int
|
||||
for i in range(wrapped_lines.size()):
|
||||
cells_needed += get_wrapped_line_trimmed_length(wrapped_lines, i, old_cols)
|
||||
|
||||
# Use src_col and scr_line to find the new wrapping point, use that to get the cells_available and
|
||||
# lines_needed
|
||||
var src_col = 0
|
||||
var src_line = 0
|
||||
var cells_available = 0
|
||||
while cells_available < cells_needed:
|
||||
if cells_needed - cells_available < new_cols:
|
||||
# Add the final line and exit the loop
|
||||
new_line_lengths.append(cells_needed - cells_available)
|
||||
break
|
||||
|
||||
src_col += new_cols
|
||||
var old_trimmed_length = get_wrapped_line_trimmed_length(wrapped_lines, src_line, old_cols)
|
||||
if src_col > old_trimmed_length:
|
||||
src_col -= old_trimmed_length
|
||||
src_line += 1
|
||||
|
||||
var ends_with_wide = wrapped_lines[src_line].get_width(src_col - 1) == 2
|
||||
if ends_with_wide:
|
||||
src_col -= 1
|
||||
|
||||
var line_length = new_cols - 1 if ends_with_wide else new_cols
|
||||
new_line_lengths.append(line_length)
|
||||
cells_available += line_length
|
||||
|
||||
return new_line_lengths
|
||||
|
||||
|
||||
static func get_wrapped_line_trimmed_length(lines: Array, i: int, cols: int) -> int:
|
||||
# If this is the last row in the wrapped line, get the actual trimmed length
|
||||
if i == lines.size() - 1:
|
||||
return lines[i].get_trimmed_length()
|
||||
|
||||
# Detect whether the following line starts with a wide character and the end of the current line
|
||||
# is null, if so then we can be pretty sure the null character should be excluded from the line
|
||||
# length
|
||||
var ends_in_null = not (lines[i].has_content(cols - 1)) and lines[i].get_width(cols - 1) == 1
|
||||
var following_line_starts_with_wide = lines[i + 1].get_width(0) == 2
|
||||
if ends_in_null and following_line_starts_with_wide:
|
||||
return cols - 1
|
||||
return cols
|
|
@ -1,69 +0,0 @@
|
|||
# Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
const Buffer = preload("res://addons/godot_xterm/buffer/buffer.gd")
|
||||
|
||||
signal buffer_activated(active_buffer, inactive_buffer)
|
||||
|
||||
var normal
|
||||
var alt
|
||||
var active
|
||||
|
||||
|
||||
func _init(options_service, buffer_service):
|
||||
normal = Buffer.new(true, options_service, buffer_service)
|
||||
normal.fill_viewport_rows()
|
||||
|
||||
# The alt buffer should never have scrollback.
|
||||
# See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
||||
alt = Buffer.new(false, options_service, buffer_service)
|
||||
active = normal
|
||||
|
||||
setup_tab_stops()
|
||||
|
||||
|
||||
# Sets the normal Bufer of the BufferSet as its currently active Buffer.
|
||||
func activate_normal_buffer() -> void:
|
||||
if active == normal:
|
||||
return
|
||||
|
||||
normal.x = alt.x
|
||||
normal.y = alt.y
|
||||
|
||||
# The alt buffer should always be cleared when we switch to the normal
|
||||
# buffer. This frees up memory since the alt buffer should always be new
|
||||
# when activated.
|
||||
alt.clear()
|
||||
active = normal
|
||||
emit_signal("buffer_activated", normal, alt)
|
||||
|
||||
|
||||
# Sets the alt Buffer of the BufferSet as its currently active Buffer.
|
||||
func activate_alt_buffer(fill_attr = null) -> void:
|
||||
if active == alt:
|
||||
return
|
||||
|
||||
# Since the alt buffer is always cleared when the normal buffer is
|
||||
# activated, we want to fill it when switching to it.
|
||||
alt.fill_viewport_rows(fill_attr)
|
||||
alt.x = normal.x
|
||||
alt.y = normal.y
|
||||
active = alt
|
||||
emit_signal("buffer_activated", alt, normal)
|
||||
|
||||
|
||||
# Resizes both normal and alt buffers, adjusting their data accordingly.
|
||||
# @param new_cols The new number of columns.
|
||||
# @param new_rows The new number of rows.
|
||||
func resize(new_cols: int, new_rows: int) -> void:
|
||||
normal.resize(new_cols, new_rows)
|
||||
alt.resize(new_cols, new_rows)
|
||||
|
||||
|
||||
# Setup the tab stops.
|
||||
# @param i The index to start setting up tab stops from.
|
||||
func setup_tab_stops(i = null) -> void:
|
||||
normal.setup_tab_stops(i)
|
||||
alt.setup_tab_stops(i)
|
|
@ -1,68 +0,0 @@
|
|||
# Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends "res://addons/godot_xterm/buffer/attribute_data.gd"
|
||||
# CellData - represents a single cell in the terminal buffer.
|
||||
|
||||
|
||||
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
|
||||
|
||||
const Content = Constants.Content
|
||||
|
||||
# Helper to create CellData from CharData
|
||||
static func from_char_data(value):
|
||||
# Workaround as per: https://github.com/godotengine/godot/issues/19345#issuecomment-471218401
|
||||
var char_data = load("res://addons/godot_xterm/buffer/cell_data.gd").new()
|
||||
char_data.set_from_char_data(value)
|
||||
return char_data
|
||||
|
||||
|
||||
# Primitives from terminal buffer
|
||||
var content = 0
|
||||
var combined_data = ''
|
||||
|
||||
|
||||
# Whether cell contains a combined string
|
||||
func is_combined() -> int:
|
||||
return content & Content.IS_COMBINED_MASK
|
||||
|
||||
|
||||
func get_width() -> int:
|
||||
return content >> Content.WIDTH_SHIFT
|
||||
|
||||
|
||||
func get_chars() -> String:
|
||||
if content & Content.IS_COMBINED_MASK:
|
||||
return combined_data
|
||||
elif content & Content.CODEPOINT_MASK:
|
||||
return char(content & Content.CODEPOINT_MASK)
|
||||
else:
|
||||
return Constants.NULL_CELL_CHAR
|
||||
|
||||
func get_code() -> int:
|
||||
if is_combined():
|
||||
return combined_data.ord_at(combined_data.length() - 1)
|
||||
else:
|
||||
return content & Content.CODEPOINT_MASK
|
||||
|
||||
|
||||
func set_from_char_data(value) -> void:
|
||||
var attr: int = value[Constants.CHAR_DATA_ATTR_INDEX]
|
||||
var character: String = value[Constants.CHAR_DATA_CHAR_INDEX]
|
||||
var width: int = value[Constants.CHAR_DATA_WIDTH_INDEX]
|
||||
var code: int = value[Constants.CHAR_DATA_CODE_INDEX]
|
||||
|
||||
fg = attr
|
||||
bg = 0
|
||||
# combined strings need special treatment. Javascript uses utf16 for strings
|
||||
# whereas Godot uses utf8, therefore we don't need any of the special
|
||||
# handling of surrogates in the original xterm.js code.
|
||||
if character.length() >= 2:
|
||||
combined_data = character
|
||||
content = Content.IS_COMBINED_MASK | (width << Content.WIDTH_SHIFT)
|
||||
else:
|
||||
content = (character.ord_at(0) if character.length() else 0) | (width << Content.WIDTH_SHIFT)
|
||||
|
||||
|
||||
func get_as_char_data():
|
||||
return [fg, get_chars(), get_width(), get_code()]
|
|
@ -1,106 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
const DEFAULT_COLOR = 256
|
||||
const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0)
|
||||
|
||||
const CHAR_DATA_ATTR_INDEX = 0
|
||||
const CHAR_DATA_CHAR_INDEX = 1
|
||||
const CHAR_DATA_WIDTH_INDEX = 2
|
||||
const CHAR_DATA_CODE_INDEX = 3
|
||||
|
||||
# Null cell - a real empty cell (containing nothing).
|
||||
# Note that code should always be 0 for a null cell as
|
||||
# several test condition of the buffer line rely on this.
|
||||
const NULL_CELL_CHAR = ''
|
||||
const NULL_CELL_WIDTH = 1
|
||||
const NULL_CELL_CODE = 0
|
||||
|
||||
|
||||
# Whitespace cell.
|
||||
# This is meant as a replacement for empty cells when needed
|
||||
# during rendering lines to preserve correct alignment.
|
||||
const WHITESPACE_CELL_CHAR = ' '
|
||||
const WHITESPACE_CELL_WIDTH = 1
|
||||
const WHITESPACE_CELL_CODE = 32
|
||||
|
||||
|
||||
# Bitmasks for accessing data in `content`.
|
||||
enum Content {
|
||||
CODEPOINT_MASK = 0x1FFFFF
|
||||
IS_COMBINED_MASK = 0x200000
|
||||
HAS_CONTENT_MASK = 0x3FFFFF
|
||||
WIDTH_MASK = 0xC00000
|
||||
WIDTH_SHIFT = 22
|
||||
}
|
||||
|
||||
|
||||
enum Attributes {
|
||||
# bit 1..8 blue in RGB, color in P256 and P16
|
||||
BLUE_MASK = 0xFF
|
||||
BLUE_SHIFT = 0
|
||||
PCOLOR_MASK = 0xFF
|
||||
PCOLOR_SHIFT = 0
|
||||
|
||||
# bit 9..16 green in RGB
|
||||
GREEN_MASK = 0xFF00
|
||||
GREEN_SHIFT = 8
|
||||
|
||||
# bit 17..24 red in RGB
|
||||
RED_MASK = 0xFF0000
|
||||
RED_SHIFT = 16
|
||||
|
||||
# bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3)
|
||||
CM_MASK = 0x3000000
|
||||
CM_DEFAULT = 0
|
||||
CM_P16 = 0x1000000
|
||||
CM_P256 = 0x2000000
|
||||
CM_RGB = 0x3000000
|
||||
|
||||
# bit 1..24 RGB room
|
||||
RGB_MASK = 0xFFFFFF
|
||||
}
|
||||
|
||||
|
||||
enum FgFlags {
|
||||
# bit 27..31 (32th bit unused)
|
||||
INVERSE = 0x4000000
|
||||
BOLD = 0x8000000
|
||||
UNDERLINE = 0x10000000
|
||||
BLINK = 0x20000000
|
||||
INVISIBLE = 0x40000000
|
||||
}
|
||||
|
||||
|
||||
enum BgFlags {
|
||||
# bit 27..32 (upper 3 unused)
|
||||
ITALIC = 0x4000000
|
||||
DIM = 0x8000000
|
||||
HAS_EXTENDED = 0x10000000
|
||||
}
|
||||
|
||||
|
||||
enum UnderlineStyle {
|
||||
NONE
|
||||
SINGLE
|
||||
DOUBLE
|
||||
CURLY
|
||||
DOTTED
|
||||
DASHED
|
||||
}
|
||||
|
||||
enum CursorStyle {
|
||||
BLOCK
|
||||
UNDERLINE
|
||||
BAR
|
||||
}
|
||||
|
||||
enum BellStyle {
|
||||
NONE
|
||||
VISUAL
|
||||
SOUND
|
||||
BOTH
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
#! /usr/bin/env nix-shell
|
||||
#! nix-shell -i bash --pure -p binutils.bintools cmake scons
|
||||
|
||||
# Make sure we are in the addons/godot_xterm_native directory
|
||||
# Make sure we are in the addons/godot_xterm directory
|
||||
cd ${BASH_SOURCE%/*}
|
||||
|
||||
# Initialize godot-cpp
|
|
@ -1,19 +0,0 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
var ch # character
|
||||
var fg = Color(1.0, 1.0, 1.0) # foreground color
|
||||
var bg = Color(0.0, 0.0, 0.0) # background color
|
||||
var ff = 0 # font flags
|
||||
|
||||
func _init(
|
||||
character: String,
|
||||
background_color: Color = bg,
|
||||
foreground_color: Color = fg,
|
||||
font_flags = ff # Does this work or will it cause problems (this assignement technique)
|
||||
):
|
||||
ch = character
|
||||
bg = background_color
|
||||
fg = foreground_color
|
||||
ff = font_flags
|
|
@ -1,185 +0,0 @@
|
|||
# Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
# Represents a circular list; a list with a maximum size that wraps around when push is called,
|
||||
# overriding values at the start of the list.
|
||||
|
||||
|
||||
signal deleted(index, amount)
|
||||
signal inserted
|
||||
signal trimmed
|
||||
|
||||
var _array
|
||||
var _start_index: int
|
||||
var length: int = 0 setget _set_length,_get_length
|
||||
var max_length: int setget _set_max_length,_get_max_length
|
||||
var is_full: bool setget ,_get_is_full
|
||||
|
||||
|
||||
func _set_length(new_length: int):
|
||||
if new_length > length:
|
||||
for i in range(length, new_length):
|
||||
_array[i] = null
|
||||
length = new_length
|
||||
|
||||
|
||||
func _get_length():
|
||||
return length
|
||||
|
||||
|
||||
func _set_max_length(new_max_length):
|
||||
if max_length == new_max_length:
|
||||
return
|
||||
|
||||
# Reconstruct array, starting at index 0.
|
||||
# Only transfer values from the indexes 0 to length.
|
||||
var new_array = []
|
||||
new_array.resize(new_max_length)
|
||||
for i in range(0, min(new_max_length, length)):
|
||||
new_array[i] = _array[_get_cyclic_index(i)]
|
||||
_array = new_array
|
||||
max_length = new_max_length
|
||||
_start_index = 0
|
||||
|
||||
|
||||
func _get_max_length():
|
||||
return max_length
|
||||
|
||||
|
||||
# Ringbuffer is at max length.
|
||||
func _get_is_full() -> bool:
|
||||
return length == max_length
|
||||
|
||||
|
||||
func _init(max_length = 0):
|
||||
self.max_length = max_length
|
||||
_array = []
|
||||
_array.resize(max_length)
|
||||
_start_index = 0
|
||||
|
||||
|
||||
func get_el(index: int):
|
||||
return _array[_get_cyclic_index(index)]
|
||||
|
||||
|
||||
# Alias for `get_al`.
|
||||
func get_line(index: int):
|
||||
return get_el(index)
|
||||
|
||||
|
||||
func set_el(index: int, value) -> void:
|
||||
_array[_get_cyclic_index(index)] = value
|
||||
|
||||
|
||||
# Alias for `set_el`.
|
||||
func set_line(index: int, value) -> void:
|
||||
set_el(index, value)
|
||||
|
||||
|
||||
# Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0
|
||||
# if the maximum length is reached.
|
||||
# @param value The value to push onto the list.
|
||||
func push(value) -> void:
|
||||
_array[_get_cyclic_index(length)] = value
|
||||
if length == max_length:
|
||||
_start_index += 1
|
||||
_start_index %= max_length
|
||||
emit_signal("trimmed", 1)
|
||||
else:
|
||||
length += 1
|
||||
|
||||
|
||||
# Advance ringbuffer index and return current element for recycling.
|
||||
# Note: The buffer must be full for this method to work.
|
||||
# @throws When the buffer is not full.
|
||||
func recycle():
|
||||
if length != max_length:
|
||||
push_error("Can only recycle when the buffer is full")
|
||||
_start_index = (_start_index + 1) % max_length
|
||||
emit_signal("trimmed", 1)
|
||||
return _array[_get_cyclic_index(length - 1)]
|
||||
|
||||
|
||||
# Removes and returns the last value on the list.
|
||||
# @return The popped value.
|
||||
func pop():
|
||||
var last = _array[_get_cyclic_index(length - 1)]
|
||||
length -= 1
|
||||
return last
|
||||
|
||||
|
||||
# Deletes and/or inserts items at a particular index (in that order). Unlike
|
||||
# Array.prototype.splice, this operation does not return the deleted items as a new array in
|
||||
# order to save creating a new array. Note that this operation may shift all values in the list
|
||||
# in the worst case.
|
||||
# @param start The index to delete and/or insert.
|
||||
# @param deleteCount The number of elements to delete.
|
||||
# @param items The items to insert.
|
||||
func splice(start: int, delete_count: int, items: Array = []) -> void:
|
||||
# Delete items
|
||||
if delete_count:
|
||||
for i in range(start, length - delete_count):
|
||||
_array[_get_cyclic_index(i)] = _array[_get_cyclic_index(i + delete_count)]
|
||||
length -= delete_count
|
||||
|
||||
# Add items
|
||||
var i = length - 1
|
||||
while i >= start:
|
||||
_array[_get_cyclic_index(i + items.size())] = _array[_get_cyclic_index(i)]
|
||||
i -= 1
|
||||
for j in range(items.size()):
|
||||
_array[_get_cyclic_index(start + j)] = items[j]
|
||||
|
||||
# Adjust length as needed
|
||||
if length + items.size() > max_length:
|
||||
var count_to_trim = (length + items.size()) - max_length
|
||||
_start_index += count_to_trim
|
||||
length = max_length
|
||||
emit_signal("trimmed", count_to_trim)
|
||||
else:
|
||||
length += items.size()
|
||||
|
||||
|
||||
# Trims a number of items from the start of the list.
|
||||
# @param count The number of items to remove.
|
||||
func trim_start(count: int) -> void:
|
||||
if count > length:
|
||||
count = length
|
||||
_start_index += count
|
||||
length -= count
|
||||
emit_signal("trimmed", count)
|
||||
|
||||
|
||||
func shift_elements(start: int, count: int, offset: int) -> void:
|
||||
if count <= 0:
|
||||
return
|
||||
if start < 0 or start >= length:
|
||||
self.push_error("start argument out of range")
|
||||
if start + offset < 0:
|
||||
self.push_error("cannot shift elements in list beyond index 0")
|
||||
|
||||
if offset > 0:
|
||||
for i in range(count - 1, -1, -1):
|
||||
set_el(start + i + offset, get_el(start + i))
|
||||
|
||||
var expand_list_by = (start + count + offset) - length
|
||||
|
||||
if expand_list_by > 0:
|
||||
length += expand_list_by
|
||||
while length > max_length:
|
||||
length -= 1
|
||||
_start_index += 1
|
||||
emit_signal("trimmed", 1)
|
||||
else:
|
||||
for i in range(0, count):
|
||||
set_el(start + i + offset, get_el(start + i))
|
||||
|
||||
|
||||
func _get_cyclic_index(index: int) -> int:
|
||||
return (_start_index + index) % max_length
|
||||
|
||||
|
||||
# Wrapper for `push_error` so we can test for calls to this built-in function.
|
||||
func push_error(message):
|
||||
push_error(message)
|
|
@ -1,108 +0,0 @@
|
|||
# Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
# Xterm.js stores colors in both css and rgba formats. In this case we only need
|
||||
# to store the colors in godots RGBA Color format.
|
||||
|
||||
|
||||
static func _generate_default_ansi_colors() -> PoolColorArray:
|
||||
var colors = PoolColorArray([
|
||||
# dark:
|
||||
Color('#2e3436'),
|
||||
Color('#cc0000'),
|
||||
Color('#4e9a06'),
|
||||
Color('#c4a000'),
|
||||
Color('#3465a4'),
|
||||
Color('#75507b'),
|
||||
Color('#06989a'),
|
||||
Color('#d3d7cf'),
|
||||
# bright:
|
||||
Color('#555753'),
|
||||
Color('#ef2929'),
|
||||
Color('#8ae234'),
|
||||
Color('#fce94f'),
|
||||
Color('#729fcf'),
|
||||
Color('#ad7fa8'),
|
||||
Color('#34e2e2'),
|
||||
Color('#eeeeec'),
|
||||
])
|
||||
|
||||
# Fill in the remaining 240 ANSI colors.
|
||||
# Generate colors (16-231)
|
||||
var v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
|
||||
for i in range(0, 216):
|
||||
var r = v[(i / 36) % 6 | 0]
|
||||
var g = v[(i / 6) % 6 | 0]
|
||||
var b = v[i % 6]
|
||||
colors.append(Color("%02x%02x%02x" % [r, g, b]))
|
||||
|
||||
# Generate greys (232-255)
|
||||
for i in range(0, 24):
|
||||
var c = 8 + i * 10
|
||||
colors.append(Color("%02x%02x%02x" % [c, c, c]))
|
||||
|
||||
return colors
|
||||
|
||||
|
||||
const DEFAULT_FOREGROUND = Color('#ffffff')
|
||||
const DEFAULT_BACKGROUND = Color('#000000')
|
||||
const DEFAULT_CURSOR = Color('#ffffff')
|
||||
const DEFAULT_CURSOR_ACCENT = Color('#000000')
|
||||
const DEFAULT_SELECTION = Color(1, 1, 1, 0.3)
|
||||
var DEFAULT_ANSI_COLORS = _generate_default_ansi_colors()
|
||||
|
||||
var colors
|
||||
var _litmus_color: Gradient = Gradient.new()
|
||||
var _contrast_cache: Dictionary = {}
|
||||
|
||||
|
||||
func _init():
|
||||
colors = {
|
||||
'foreground': DEFAULT_FOREGROUND,
|
||||
'background': DEFAULT_BACKGROUND,
|
||||
'cursor': DEFAULT_CURSOR,
|
||||
'cursor_accent': DEFAULT_CURSOR_ACCENT,
|
||||
'selection': DEFAULT_SELECTION,
|
||||
'selection_opaque': DEFAULT_BACKGROUND.blend(DEFAULT_SELECTION),
|
||||
'ansi': DEFAULT_ANSI_COLORS,
|
||||
'contrast_cache': _contrast_cache,
|
||||
}
|
||||
|
||||
|
||||
func on_options_change(key: String) -> void:
|
||||
if key == 'minimum_contrast_ratio':
|
||||
_contrast_cache.clear()
|
||||
|
||||
|
||||
# Sets the terminal's theme.
|
||||
# If a partial theme is provided then default
|
||||
# colors will be used where colors are not defined.
|
||||
func set_theme(theme: Dictionary = {}) -> void:
|
||||
colors['foreground'] = theme.get('foreground', DEFAULT_FOREGROUND)
|
||||
colors['bakcground'] = theme.get('background', DEFAULT_BACKGROUND)
|
||||
colors['cursor'] = theme.get('cursor', DEFAULT_CURSOR)
|
||||
colors['cursor_accent'] = theme.get('cursor_accent', DEFAULT_CURSOR_ACCENT)
|
||||
colors['selection'] = theme.get('selection', DEFAULT_SELECTION)
|
||||
colors['selection_opaque'] = theme.get('selection_opaque', colors['selection_opaque'])
|
||||
colors['ansi'][0] = theme.get('black', DEFAULT_ANSI_COLORS[0])
|
||||
colors['ansi'][1] = theme.get('red', DEFAULT_ANSI_COLORS[1])
|
||||
colors['ansi'][2] = theme.get('green', DEFAULT_ANSI_COLORS[2])
|
||||
colors['ansi'][3] = theme.get('yellow', DEFAULT_ANSI_COLORS[3])
|
||||
colors['ansi'][4] = theme.get('blue', DEFAULT_ANSI_COLORS[4])
|
||||
colors['ansi'][5] = theme.get('magenta', DEFAULT_ANSI_COLORS[5])
|
||||
colors['ansi'][6] = theme.get('cyan', DEFAULT_ANSI_COLORS[6])
|
||||
colors['ansi'][7] = theme.get('white', DEFAULT_ANSI_COLORS[7])
|
||||
colors['ansi'][8] = theme.get('bright_black', DEFAULT_ANSI_COLORS[8])
|
||||
colors['ansi'][9] = theme.get('bright_red', DEFAULT_ANSI_COLORS[9])
|
||||
colors['ansi'][10] = theme.get('bright_green', DEFAULT_ANSI_COLORS[10])
|
||||
colors['ansi'][11] = theme.get('bright_yellow', DEFAULT_ANSI_COLORS[11])
|
||||
colors['ansi'][12] = theme.get('bright_blue', DEFAULT_ANSI_COLORS[12])
|
||||
colors['ansi'][13] = theme.get('bright_magenta', DEFAULT_ANSI_COLORS[13])
|
||||
colors['ansi'][14] = theme.get('bright_cyan', DEFAULT_ANSI_COLORS[14])
|
||||
colors['ansi'][15] = theme.get('bright_white', DEFAULT_ANSI_COLORS[15])
|
||||
|
||||
_contrast_cache.clear()
|
||||
|
||||
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
# Copyrigth (c) 2016 The xterm.js authors. All rights reserved
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
# The character sets supported by the terminal. These enable several languages
|
||||
# to be represented within the terminal with only 8-bit encoding. See ISO 2022
|
||||
# for a discussion on character sets. Only VT100 character sets are supported.
|
||||
|
||||
const CHARSETS = {
|
||||
# DEC Special Character and Line Drawing Set.
|
||||
# Reference: http:#vt100.net/docs/vt102-ug/table5-13.html
|
||||
# A lot of curses apps use this if they see TERM=xterm.
|
||||
# testing: echo -e "\e(0a\e(B"
|
||||
# The xterm output sometimes seems to conflict with the
|
||||
# reference above. xterm seems in line with the reference
|
||||
# when running vttest however.
|
||||
# The table below now uses xterm"s output from vttest.
|
||||
"0": {
|
||||
"`": "\u25c6", # "◆"
|
||||
"a": "\u2592", # "▒"
|
||||
"b": "\u2409", # "␉" (HT)
|
||||
"c": "\u240c", # "␌" (FF)
|
||||
"d": "\u240d", # "␍" (CR)
|
||||
"e": "\u240a", # "␊" (LF)
|
||||
"f": "\u00b0", # "°"
|
||||
"g": "\u00b1", # "±"
|
||||
"h": "\u2424", # "" (NL)
|
||||
"i": "\u240b", # "␋" (VT)
|
||||
"j": "\u2518", # "┘"
|
||||
"k": "\u2510", # "┐"
|
||||
"l": "\u250c", # "┌"
|
||||
"m": "\u2514", # "└"
|
||||
"n": "\u253c", # "┼"
|
||||
"o": "\u23ba", # "⎺"
|
||||
"p": "\u23bb", # "⎻"
|
||||
"q": "\u2500", # "─"
|
||||
"r": "\u23bc", # "⎼"
|
||||
"s": "\u23bd", # "⎽"
|
||||
"t": "\u251c", # "├"
|
||||
"u": "\u2524", # "┤"
|
||||
"v": "\u2534", # "┴"
|
||||
"w": "\u252c", # "┬"
|
||||
"x": "\u2502", # "│"
|
||||
"y": "\u2264", # "≤"
|
||||
"z": "\u2265", # "≥"
|
||||
"{": "\u03c0", # "π"
|
||||
"|": "\u2260", # "≠"
|
||||
"}": "\u00a3", # "£"
|
||||
"~": "\u00b7" # "·"
|
||||
},
|
||||
|
||||
# British character set
|
||||
# ESC (A
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-5.html
|
||||
"A": {
|
||||
"#": "£"
|
||||
},
|
||||
|
||||
# United States character set
|
||||
# ESC (B
|
||||
"B": null,
|
||||
|
||||
# Dutch character set
|
||||
# ESC (4
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-6.html
|
||||
"4": {
|
||||
"#": "£",
|
||||
"@": "¾",
|
||||
"[": "ij",
|
||||
"\\": "½",
|
||||
"]": "|",
|
||||
"{": "¨",
|
||||
"|": "f",
|
||||
"}": "¼",
|
||||
"~": "´"
|
||||
},
|
||||
|
||||
# Finnish character set
|
||||
# ESC (C or ESC (5
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-7.html
|
||||
"C": {
|
||||
"[": "Ä",
|
||||
"\\": "Ö",
|
||||
"]": "Å",
|
||||
"^": "Ü",
|
||||
"`": "é",
|
||||
"{": "ä",
|
||||
"|": "ö",
|
||||
"}": "å",
|
||||
"~": "ü"
|
||||
},
|
||||
"5": {
|
||||
"[": "Ä",
|
||||
"\\": "Ö",
|
||||
"]": "Å",
|
||||
"^": "Ü",
|
||||
"`": "é",
|
||||
"{": "ä",
|
||||
"|": "ö",
|
||||
"}": "å",
|
||||
"~": "ü"
|
||||
},
|
||||
|
||||
# French character set
|
||||
# ESC (R
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-8.html
|
||||
"R": {
|
||||
"#": "£",
|
||||
"@": "à",
|
||||
"[": "°",
|
||||
"\\": "ç",
|
||||
"]": "§",
|
||||
"{": "é",
|
||||
"|": "ù",
|
||||
"}": "è",
|
||||
"~": "¨"
|
||||
},
|
||||
|
||||
# French Canadian character set
|
||||
# ESC (Q
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-9.html
|
||||
"Q": {
|
||||
"@": "à",
|
||||
"[": "â",
|
||||
"\\": "ç",
|
||||
"]": "ê",
|
||||
"^": "î",
|
||||
"`": "ô",
|
||||
"{": "é",
|
||||
"|": "ù",
|
||||
"}": "è",
|
||||
"~": "û"
|
||||
},
|
||||
|
||||
# German character set
|
||||
# ESC (K
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-10.html
|
||||
"K": {
|
||||
"@": "§",
|
||||
"[": "Ä",
|
||||
"\\": "Ö",
|
||||
"]": "Ü",
|
||||
"{": "ä",
|
||||
"|": "ö",
|
||||
"}": "ü",
|
||||
"~": "ß"
|
||||
},
|
||||
|
||||
# Italian character set
|
||||
# ESC (Y
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-11.html
|
||||
"Y": {
|
||||
"#": "£",
|
||||
"@": "§",
|
||||
"[": "°",
|
||||
"\\": "ç",
|
||||
"]": "é",
|
||||
"`": "ù",
|
||||
"{": "à",
|
||||
"|": "ò",
|
||||
"}": "è",
|
||||
"~": "ì"
|
||||
},
|
||||
|
||||
# Norwegian/Danish character set
|
||||
# ESC (E or ESC (6
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-12.html
|
||||
"E": {
|
||||
"@": "Ä",
|
||||
"[": "Æ",
|
||||
"\\": "Ø",
|
||||
"]": "Å",
|
||||
"^": "Ü",
|
||||
"`": "ä",
|
||||
"{": "æ",
|
||||
"|": "ø",
|
||||
"}": "å",
|
||||
"~": "ü"
|
||||
},
|
||||
"6": {
|
||||
"@": "Ä",
|
||||
"[": "Æ",
|
||||
"\\": "Ø",
|
||||
"]": "Å",
|
||||
"^": "Ü",
|
||||
"`": "ä",
|
||||
"{": "æ",
|
||||
"|": "ø",
|
||||
"}": "å",
|
||||
"~": "ü"
|
||||
},
|
||||
|
||||
# Spanish character set
|
||||
# ESC (Z
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-13.html
|
||||
"Z": {
|
||||
"#": "£",
|
||||
"@": "§",
|
||||
"[": "¡",
|
||||
"\\": "Ñ",
|
||||
"]": "¿",
|
||||
"{": "°",
|
||||
"|": "ñ",
|
||||
"}": "ç"
|
||||
},
|
||||
|
||||
# Swedish character set
|
||||
# ESC (H or ESC (7
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-14.html
|
||||
"H": {
|
||||
"@": "É",
|
||||
"[": "Ä",
|
||||
"\\": "Ö",
|
||||
"]": "Å",
|
||||
"^": "Ü",
|
||||
"`": "é",
|
||||
"{": "ä",
|
||||
"|": "ö",
|
||||
"}": "å",
|
||||
"~": "ü"
|
||||
},
|
||||
"7": {
|
||||
"@": "É",
|
||||
"[": "Ä",
|
||||
"\\": "Ö",
|
||||
"]": "Å",
|
||||
"^": "Ü",
|
||||
"`": "é",
|
||||
"{": "ä",
|
||||
"|": "ö",
|
||||
"}": "å",
|
||||
"~": "ü"
|
||||
},
|
||||
|
||||
# Swiss character set
|
||||
# ESC (=
|
||||
# Reference: http:#vt100.net/docs/vt220-rm/table2-15.html
|
||||
#/
|
||||
"=": {
|
||||
"#": "ù",
|
||||
"@": "à",
|
||||
"[": "é",
|
||||
"\\": "ç",
|
||||
"]": "ê",
|
||||
"^": "î",
|
||||
"_": "è",
|
||||
"`": "ô",
|
||||
"{": "ä",
|
||||
"|": "ö",
|
||||
"}": "ü",
|
||||
"~": "û"
|
||||
},
|
||||
}
|
||||
|
||||
# The default character set, US.
|
||||
const DEFAULT_CHARSET = CHARSETS["B"]
|
|
@ -1,93 +0,0 @@
|
|||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@ -1,6 +0,0 @@
|
|||
[gd_resource type="DynamicFont" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.ttf" type="DynamicFontData" id=1]
|
||||
|
||||
[resource]
|
||||
font_data = ExtResource( 1 )
|
Binary file not shown.
|
@ -1,6 +0,0 @@
|
|||
[gd_resource type="DynamicFont" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold_italic.ttf" type="DynamicFontData" id=1]
|
||||
|
||||
[resource]
|
||||
font_data = ExtResource( 1 )
|
Binary file not shown.
|
@ -1,6 +0,0 @@
|
|||
[gd_resource type="DynamicFont" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_italic.ttf" type="DynamicFontData" id=1]
|
||||
|
||||
[resource]
|
||||
font_data = ExtResource( 1 )
|
Binary file not shown.
|
@ -1,6 +0,0 @@
|
|||
[gd_resource type="DynamicFont" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.ttf" type="DynamicFontData" id=1]
|
||||
|
||||
[resource]
|
||||
font_data = ExtResource( 1 )
|
Binary file not shown.
|
@ -1,93 +0,0 @@
|
|||
Copyright 2011, The VT323 Project Authors (peter.hull@oikoi.com)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@ -1,3 +0,0 @@
|
|||
[gd_resource type="DynamicFont" format=2]
|
||||
|
||||
[resource]
|
Binary file not shown.
16
addons/godot_xterm/godotxtermnative.gdnlib
Normal file
16
addons/godot_xterm/godotxtermnative.gdnlib
Normal file
|
@ -0,0 +1,16 @@
|
|||
[general]
|
||||
|
||||
singleton=false
|
||||
load_once=true
|
||||
symbol_prefix="godot_"
|
||||
reloadable=true
|
||||
|
||||
[entry]
|
||||
|
||||
X11.64="res://addons/godot_xterm/bin/x11/libgodotxtermnative.so"
|
||||
Server.64="res://addons/godot_xterm/bin/x11/libgodotxtermnative.so"
|
||||
|
||||
[dependencies]
|
||||
|
||||
X11.64=[ ]
|
||||
Server.64=[ ]
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path d="m4.5605 3.9746c-0.074558 0-0.14819 0.029049-0.20508 0.085938l-0.27539 0.27539c-0.11354 0.11358-0.11332 0.29631 0 0.41016l1.8691 1.8789-1.8691 1.8789c-0.11336 0.11385-0.11358 0.29657 0 0.41016l0.27539 0.27539c0.11377 0.11378 0.29833 0.11378 0.41211 0l2.3594-2.3594c0.11378-0.11378 0.11378-0.29834 0-0.41211l-2.3594-2.3574c-0.056882-0.056888-0.13247-0.085938-0.20703-0.085938zm3.2207 4.3984c-0.1609 0-0.29102 0.13012-0.29102 0.29102v0.38867c0 0.1609 0.13012 0.29102 0.29102 0.29102h3.6914c0.1609 0 0.29102-0.13012 0.29102-0.29102v-0.38867c0-0.1609-0.13012-0.29102-0.29102-0.29102z" fill="#a5efac" stroke-width=".012139"/>
|
||||
<path d="m3 1c-1.1046 0-2 0.8954-2 2v10c0 1.1046 0.89543 2 2 2h10c1.1046 0 2-0.8954 2-2v-10c0-1.1046-0.89543-2-2-2zm0 2h10v10h-10z" fill="#a5efac"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1,266 +0,0 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors.
|
||||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
# Convert a given to a utf8 PoolByteArray.
|
||||
# The code for this function is based on the stackoverflow
|
||||
# answer by user Schwern https://stackoverflow.com/a/42013984.
|
||||
static func utf32_to_utf8(codepoint: int):
|
||||
var utf8 = PoolByteArray([])
|
||||
|
||||
if codepoint <= 0x007F:
|
||||
utf8.append(codepoint)
|
||||
elif codepoint <= 0x07FF:
|
||||
utf8.append(0b11000000 | codepoint >> 6 & 0b00011111)
|
||||
utf8.append(0b10000000 | codepoint & 0b00111111)
|
||||
elif codepoint <= 0xFFFF:
|
||||
utf8.append(0b11100000 | codepoint >> 12 & 0b00001111)
|
||||
utf8.append(0b10000000 | codepoint >> 6 & 0b00111111)
|
||||
utf8.append(0b10000000 | codepoint & 0b00111111)
|
||||
elif codepoint <= 0x10FFFF:
|
||||
utf8.append(0b11110000 | codepoint >> 18 & 0b00000111)
|
||||
utf8.append(0b10000000 | codepoint >> 12 & 0b00111111)
|
||||
utf8.append(0b10000000 | codepoint >> 6 & 0b00111111)
|
||||
utf8.append(0b10000000 | codepoint & 0b00111111)
|
||||
else:
|
||||
push_warning("Codepoint " + String(codepoint) + " is out of UTF-8 range")
|
||||
|
||||
return utf8
|
||||
|
||||
|
||||
# Covert UTF32 char codes into a String.
|
||||
# Basically the same as `char` but for multiple codepoints
|
||||
# in a loop (which is a lot faster).
|
||||
static func utf32_to_string(data: Array, start: int = 0, end: int = -1):
|
||||
if end == -1:
|
||||
end = data.size()
|
||||
var result = ''
|
||||
for i in range(start, end):
|
||||
result += char(data[i])
|
||||
return result
|
||||
|
||||
|
||||
# Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.
|
||||
class Utf8ToUtf32:
|
||||
var interim = PoolByteArray()
|
||||
|
||||
func _init():
|
||||
interim.resize(3)
|
||||
|
||||
# Clears interim bytes and resets decoder to clean state.
|
||||
func clear():
|
||||
for i in interim.size():
|
||||
interim[i] = 0
|
||||
|
||||
# Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`.
|
||||
# The methods assumes stream input and will store partly transmitted bytes
|
||||
# and decode them with the next data chunk.
|
||||
# Note: The method does no bound checks for target, therefore make sure
|
||||
# the provided data chunk does not exceed the size of `target`.
|
||||
# Returns the number of written codepoints in `target`.
|
||||
func decode(input: PoolByteArray, target: Array):
|
||||
var length = input.size()
|
||||
|
||||
if !length:
|
||||
return 0
|
||||
|
||||
if length > target.size():
|
||||
target.resize(length)
|
||||
|
||||
var size = 0
|
||||
var byte1: int
|
||||
var byte2: int
|
||||
var byte3: int
|
||||
var byte4: int
|
||||
var codepoint = 0
|
||||
var start_pos = 0
|
||||
|
||||
# handle leftover bytes
|
||||
if interim[0]:
|
||||
var discard_interim = false
|
||||
var cp = interim[0]
|
||||
cp &= 0x1F if (cp & 0xE0) == 0xC0 else 0x0F if (cp & 0xF0) == 0xE0 else 0x07
|
||||
var pos = 1
|
||||
var tmp = interim[pos] & 0x3F
|
||||
while tmp && pos < 4:
|
||||
cp <<= 6
|
||||
cp |= tmp
|
||||
pos += 1
|
||||
tmp = interim[pos] & 0x3F if interim.size() < pos else 0
|
||||
# missing bytes - read from input
|
||||
var type = 2 if (interim[0] & 0xE0) == 0xC0 else 3 if (interim[0] & 0xF0) == 0xE0 else 4
|
||||
var missing = type - pos
|
||||
while start_pos < missing:
|
||||
if start_pos >= length:
|
||||
return 0
|
||||
tmp = input[start_pos]
|
||||
start_pos += 1
|
||||
if (tmp & 0xC0) != 0x80:
|
||||
# wrong continuation, discard interim bytes completely
|
||||
start_pos -= 1
|
||||
discard_interim = true
|
||||
break
|
||||
else:
|
||||
# need to save so we can continue short inputs in next call
|
||||
interim[pos + 1] = tmp
|
||||
pos += 1
|
||||
cp <<= 6
|
||||
cp |= tmp & 0x3F
|
||||
if not discard_interim:
|
||||
# final test is type dependent
|
||||
match type:
|
||||
2:
|
||||
if cp < 0x80:
|
||||
# wrong starter byte
|
||||
start_pos -= 1
|
||||
else:
|
||||
target[size] = cp
|
||||
size += 1
|
||||
3:
|
||||
if cp < 0x0800 or (cp >= 0xD800 and cp <= 0xDFFF):
|
||||
# illegal codepoint
|
||||
pass
|
||||
else:
|
||||
target[size] = cp
|
||||
size += 1
|
||||
_:
|
||||
if cp < 0x10000 or cp > 0x10FFFF:
|
||||
# illegal codepoint
|
||||
pass
|
||||
else:
|
||||
target[size] = cp
|
||||
size += 1
|
||||
clear()
|
||||
|
||||
# loop through input
|
||||
var four_stop = length - 4
|
||||
var i = start_pos
|
||||
while i < length:
|
||||
# ASCII shortcut with loop unrolled to 4 consecutive ASCII chars.
|
||||
# This is a compromise between speed gain for ASCII
|
||||
# and penalty for non ASCII:
|
||||
# For best ASCII performance the char should be stored directly into target,
|
||||
# but even a single attempt to write to target and compare afterwards
|
||||
# penalizes non ASCII really bad (-50%), thus we load the char into byteX first,
|
||||
# which reduces ASCII performance by ~15%.
|
||||
# This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible
|
||||
# compared to the gains.
|
||||
# Note that this optimization only takes place for 4 consecutive ASCII chars,
|
||||
# for any shorter it bails out. Worst case - all 4 bytes being read but
|
||||
# thrown away due to the last being a non ASCII char (-10% performance).
|
||||
while i < four_stop:
|
||||
byte1 = input[i]
|
||||
byte2 = input[i + 1]
|
||||
byte3 = input[i + 2]
|
||||
byte4 = input[i + 3]
|
||||
if not (byte1 & 0x80) | (byte2 & 0x80) | (byte3 & 0x80) | (byte4 & 0x80):
|
||||
target[size] = byte1
|
||||
target[size+1] = byte2
|
||||
target[size+2] = byte3
|
||||
target[size+3] = byte4
|
||||
size += 4
|
||||
i += 4
|
||||
else:
|
||||
break
|
||||
|
||||
# reread byte1
|
||||
byte1 = input[i]
|
||||
i += 1
|
||||
|
||||
# 1 byte
|
||||
if byte1 < 0x80:
|
||||
target[size] = byte1
|
||||
size += 1
|
||||
|
||||
# 2 bytes
|
||||
elif (byte1 & 0xE0) == 0xC0:
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
return size
|
||||
byte2 = input[i]
|
||||
i+=1
|
||||
if (byte2 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i-=1
|
||||
continue
|
||||
codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F)
|
||||
if (codepoint < 0x80):
|
||||
# wrong starter byte
|
||||
i-=1
|
||||
continue
|
||||
target[size] = codepoint
|
||||
size+=1
|
||||
|
||||
# 3 bytes
|
||||
elif (byte1 & 0xF0) == 0xE0:
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
return size
|
||||
byte2 = input[i]
|
||||
i+=1
|
||||
if (byte2 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i-=1
|
||||
continue
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
interim[1] = byte2
|
||||
return size
|
||||
byte3 = input[i]
|
||||
i+=1
|
||||
if (byte3 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i-=1
|
||||
continue
|
||||
codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F)
|
||||
if codepoint < 0x0800 or (codepoint >=0xD800 and codepoint <= 0xDFFF):
|
||||
# illegal codepoint, no i-- here
|
||||
continue
|
||||
target[size] = codepoint
|
||||
size+=1
|
||||
|
||||
# 4 bytes
|
||||
elif (byte1 & 0xF8) == 0xF0:
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
return size
|
||||
byte2 = input[i]
|
||||
i += 1
|
||||
if (byte2 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i -= 1
|
||||
continue
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
interim[1] = byte2
|
||||
return size
|
||||
byte3 = input[i]
|
||||
i += 1
|
||||
if (byte3 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i -= 1
|
||||
continue
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
interim[1] = byte2
|
||||
interim[2] = byte3
|
||||
return size
|
||||
byte4 = input[i]
|
||||
i += 1
|
||||
if (byte4 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i -= 1
|
||||
continue
|
||||
codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F)
|
||||
if codepoint < 0x010000 or codepoint > 0x10FFFF:
|
||||
# illegal codepoint, no i-- here
|
||||
continue
|
||||
target[size] = codepoint
|
||||
size += 1
|
||||
else:
|
||||
# illegal byte, just skip
|
||||
pass
|
||||
|
||||
target.resize(size)
|
||||
return size
|
File diff suppressed because it is too large
Load diff
|
@ -1,131 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
# Psuedo-character placeholder for non-ascii characters (unicode).
|
||||
const NON_ASCII_PRINTABLE = 0xa0
|
||||
|
||||
# Payload limit for OSC and DCS.
|
||||
const PAYLOAD_LIMIT = 10000000
|
||||
|
||||
# Internal states of EscapeSequenceParser.
|
||||
enum ParserState {
|
||||
GROUND
|
||||
ESCAPE
|
||||
ESCAPE_INTERMEDIATE
|
||||
CSI_ENTRY
|
||||
CSI_PARAM
|
||||
CSI_INTERMEDIATE
|
||||
CSI_IGNORE
|
||||
SOS_PM_APC_STRING
|
||||
OSC_STRING
|
||||
DCS_ENTRY
|
||||
DCS_PARAM
|
||||
DCS_IGNORE
|
||||
DCS_INTERMEDIATE
|
||||
DCS_PASSTHROUGH
|
||||
}
|
||||
|
||||
# Internal actions of EscapeSequenceParser.
|
||||
enum ParserAction {
|
||||
IGNORE
|
||||
ERROR
|
||||
PRINT
|
||||
EXECUTE
|
||||
OSC_START
|
||||
OSC_PUT
|
||||
OSC_END
|
||||
CSI_DISPATCH
|
||||
PARAM
|
||||
COLLECT
|
||||
ESC_DISPATCH
|
||||
CLEAR
|
||||
DCS_HOOK
|
||||
DCS_PUT
|
||||
DCS_UNHOOK
|
||||
}
|
||||
|
||||
# Internal states of OscParser.
|
||||
enum OscState {
|
||||
START
|
||||
ID
|
||||
PAYLOAD
|
||||
ABORT
|
||||
}
|
||||
|
||||
# C0 control codes
|
||||
# See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C0_controls
|
||||
enum C0 {
|
||||
NUL
|
||||
SOH
|
||||
STX
|
||||
ETX
|
||||
EOT
|
||||
ENQ
|
||||
ACK
|
||||
BEL
|
||||
BS
|
||||
HT
|
||||
LF
|
||||
VT
|
||||
FF
|
||||
CR
|
||||
SO
|
||||
SI
|
||||
DLE
|
||||
DC1
|
||||
DC2
|
||||
DC3
|
||||
DC4
|
||||
NAK
|
||||
SYN
|
||||
ETB
|
||||
CAN
|
||||
EM
|
||||
SUB
|
||||
ESC
|
||||
FS
|
||||
GS
|
||||
RS
|
||||
US
|
||||
SP
|
||||
DEL = 0x7f
|
||||
}
|
||||
|
||||
# C1 control codes
|
||||
# See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C1_controls
|
||||
enum C1 {
|
||||
PAD = 0x80
|
||||
HOP = 0x81
|
||||
BPH = 0x82
|
||||
NBH = 0x83
|
||||
IND = 0x84
|
||||
NEL = 0x85
|
||||
SSA = 0x86
|
||||
ESA = 0x87
|
||||
HTS = 0x88
|
||||
HTJ = 0x89
|
||||
VTS = 0x8a
|
||||
PLD = 0x8b
|
||||
PLU = 0x8c
|
||||
RI = 0x8d
|
||||
SS2 = 0x8e
|
||||
SS3 = 0x8f
|
||||
DCS = 0x90
|
||||
PU1 = 0x91
|
||||
PU2 = 0x92
|
||||
STS = 0x93
|
||||
CCH = 0x94
|
||||
MW = 0x95
|
||||
SPA = 0x96
|
||||
EPA = 0x97
|
||||
SOS = 0x98
|
||||
SGCI = 0x99
|
||||
SCI = 0x9a
|
||||
CSI = 0x9b
|
||||
ST = 0x9c
|
||||
OSC = 0x9d
|
||||
PM = 0x9e
|
||||
APC = 0x9f
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
|
||||
|
||||
|
||||
const EMPTY_HANDLERS = []
|
||||
|
||||
|
||||
var _handlers: Dictionary = {}
|
||||
var _active: Array = EMPTY_HANDLERS
|
||||
var _ident: int = 0
|
||||
var _handler_fb: Dictionary
|
||||
|
||||
|
||||
func _init():
|
||||
pass
|
||||
|
||||
|
||||
func set_handler(ident: int, handler):
|
||||
_handlers[ident] = [handler]
|
||||
|
||||
|
||||
func clear_handler(ident: int):
|
||||
_handlers.erase(ident)
|
||||
|
||||
|
||||
func set_handler_fallback(target, method):
|
||||
_handler_fb = {'target': target, 'method': method}
|
||||
|
||||
|
||||
func reset():
|
||||
if _active.size():
|
||||
unhook(false)
|
||||
_active = EMPTY_HANDLERS
|
||||
_ident = 0
|
||||
|
||||
|
||||
func hook(ident: int, params):
|
||||
# always reset leftover handlers
|
||||
reset()
|
||||
_ident = ident
|
||||
_active = _handlers[ident] if _handlers.has(ident) else EMPTY_HANDLERS
|
||||
if _active.empty():
|
||||
_handler_fb['target'].call(_handler_fb['method'], _ident, 'HOOK', params)
|
||||
else:
|
||||
_active.invert()
|
||||
for handler in _active:
|
||||
handler.hook(params)
|
||||
_active.invert()
|
||||
|
||||
|
||||
func put(data: Array, start: int, end: int):
|
||||
if _active.empty():
|
||||
_handler_fb['target'].call(_handler_fb['method'], _ident, 'PUT',
|
||||
Decoder.utf32_to_string(data, start, end))
|
||||
else:
|
||||
_active.invert()
|
||||
for handler in _active:
|
||||
handler.put(data, start, end)
|
||||
_active.invert()
|
||||
|
||||
|
||||
func unhook(success: bool):
|
||||
if _active.empty():
|
||||
_handler_fb['target'].call(_handler_fb['method'], _ident, 'UNHOOK', success)
|
||||
else:
|
||||
_active.invert()
|
||||
for handler in _active:
|
||||
if handler.unhook(success) != false:
|
||||
success = false # will cleanup left over handlers
|
||||
_active.invert()
|
||||
_active = EMPTY_HANDLERS
|
||||
_ident = 0
|
|
@ -1,337 +0,0 @@
|
|||
# Copyright (c) 2018 The xterm.js authers. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
|
||||
const TransitionTable = preload("res://addons/godot_xterm/parser/transition_table.gd")
|
||||
const VT500TransitionTable = preload("res://addons/godot_xterm/parser/vt500_transition_table.gd")
|
||||
const DcsParser = preload("res://addons/godot_xterm/parser/dcs_parser.gd")
|
||||
const Params = preload("res://addons/godot_xterm/parser/params.gd")
|
||||
|
||||
const TableAccess = TransitionTable.TableAccess
|
||||
const NON_ASCII_PRINTABLE = Constants.NON_ASCII_PRINTABLE
|
||||
const ParserState = Constants.ParserState
|
||||
const ParserAction = Constants.ParserAction
|
||||
|
||||
var initial_state
|
||||
var current_state
|
||||
var preceding_codepoint
|
||||
|
||||
var _transitions
|
||||
|
||||
# buffers over several parse calls
|
||||
var _params
|
||||
var _collect
|
||||
|
||||
# handler lookup containers
|
||||
var _print_handler
|
||||
var _execute_handlers
|
||||
var _csi_handlers
|
||||
var _esc_handlers
|
||||
var _osc_parser
|
||||
var _dcs_parser
|
||||
var _error_handler
|
||||
|
||||
# fallback handlers
|
||||
var _print_handler_fb
|
||||
var _execute_handler_fb
|
||||
var _csi_handler_fb
|
||||
var _esc_handler_fb
|
||||
var _error_handler_fb
|
||||
|
||||
|
||||
# Default do noting fallback handler.
|
||||
# Allows a variable number of arguments from 0 - 7.
|
||||
func noop(a = null, b = null, c = null, d = null, e = null, f = null, g = null):
|
||||
pass
|
||||
|
||||
|
||||
func _init(transitions = VT500TransitionTable.new().table):
|
||||
initial_state = ParserState.GROUND
|
||||
current_state = initial_state
|
||||
_transitions = transitions
|
||||
_params = Params.new() # Defaults to 32 storable params/subparams
|
||||
_params.add_param(0) # ZDM (Zero Default Mode
|
||||
_collect = 0
|
||||
preceding_codepoint = 0
|
||||
|
||||
# set default fallback handlers and handler lookup containers
|
||||
var noop = {'target': self, 'method': 'noop'}
|
||||
_print_handler_fb = noop
|
||||
_execute_handler_fb = noop
|
||||
_csi_handler_fb = noop
|
||||
_esc_handler_fb = noop
|
||||
_error_handler_fb = noop
|
||||
_print_handler = _print_handler_fb
|
||||
_execute_handlers = {}
|
||||
_csi_handlers = {}
|
||||
_esc_handlers = {}
|
||||
_osc_parser = null # TODO OscParser.new()
|
||||
_dcs_parser = DcsParser.new()
|
||||
_error_handler = _error_handler_fb
|
||||
|
||||
# swallow 7bit ST (ESC+\)
|
||||
set_esc_handler({'final': '\\'}, self, 'noop')
|
||||
|
||||
|
||||
static func identifier(id: Dictionary, final_range: Array = [0x40, 0x7e]):
|
||||
var res = 0
|
||||
|
||||
var prefix = id.get('prefix')
|
||||
var intermediates = id.get('intermediates')
|
||||
var final = id.get('final')
|
||||
|
||||
if prefix:
|
||||
if prefix.length() > 1:
|
||||
push_error("only one byte prefix supported")
|
||||
res = prefix.to_ascii()[0]
|
||||
if res and 0x3c > res or res > 0x3f:
|
||||
push_error("prefix must be in the range 0x3c-0x3f")
|
||||
|
||||
if intermediates:
|
||||
if intermediates.length() > 2:
|
||||
push_error("only two bytes as intermediates are supported")
|
||||
for intermediate in intermediates:
|
||||
var im = intermediate.to_ascii()[0]
|
||||
if 0x20 > im or im > 0x2f:
|
||||
push_error("intermediate must be in the range 0x20-0x2f")
|
||||
res = res << 8
|
||||
res = res | im
|
||||
|
||||
if final.length() != 1:
|
||||
push_error("final must be a single byte")
|
||||
var final_code = final.to_ascii()[0]
|
||||
if final_range[0] > final_code or final_code > final_range[1]:
|
||||
push_error("final must be in the range " + String(final_range[0]) + "-" + String(final_range[1]))
|
||||
res = res << 8
|
||||
res = res | final_code
|
||||
|
||||
return res
|
||||
|
||||
static func ident_to_string(ident: int):
|
||||
var res = PoolStringArray([])
|
||||
while ident:
|
||||
res.append(PoolByteArray([ident & 0xFF]).get_string_from_ascii())
|
||||
ident >>= 8
|
||||
res.invert()
|
||||
return res.join('')
|
||||
|
||||
func set_print_handler(target: Object, method: String):
|
||||
_print_handler = { 'target': target, 'method': method }
|
||||
|
||||
|
||||
func add_esc_handler(id, target, method):
|
||||
var ident = identifier(id, [0x30, 0x7e])
|
||||
if not _esc_handlers.has(ident):
|
||||
_esc_handlers[ident] = []
|
||||
var handler_list = _esc_handlers[ident]
|
||||
handler_list.append({'target': target, 'method': method})
|
||||
|
||||
|
||||
func set_csi_handler(id: Dictionary, target: Object, method: String):
|
||||
_csi_handlers[identifier(id)] = [{ 'target': target, 'method': method }]
|
||||
|
||||
|
||||
func set_csi_handler_fallback(target, method):
|
||||
_csi_handler_fb = { 'target': target, 'method': method }
|
||||
|
||||
|
||||
func set_execute_handler(flag: int, target: Object, method: String):
|
||||
_execute_handlers[flag] = { 'target': target, 'method': method }
|
||||
|
||||
|
||||
func set_execute_handler_fallback(target: Object, method: String):
|
||||
_execute_handler_fb = { 'target': target, 'method': method }
|
||||
|
||||
|
||||
func set_esc_handler(id, target, method, arg = null):
|
||||
_esc_handlers[identifier(id, [0x30, 0x7e])] = [{'target': target,
|
||||
'method': method, 'arg': arg}]
|
||||
|
||||
|
||||
func set_esc_handler_fallback(target: Object, method: String):
|
||||
_esc_handler_fb = {'target': target, 'method': method}
|
||||
|
||||
|
||||
func add_dcs_handler(id, target, method):
|
||||
pass
|
||||
# TODO!!!
|
||||
|
||||
func set_dcs_handler(id, target: Object, method: String):
|
||||
_dcs_parser.set_handler(id, {'target': target, 'method': method})
|
||||
|
||||
func set_dcs_handler_fallback(target: Object, method: String):
|
||||
_dcs_parser.set_handler_fallback(target, method)
|
||||
|
||||
func reset():
|
||||
current_state = initial_state
|
||||
_params.reset()
|
||||
_params.add_param(0) # ZDM
|
||||
_collect = 0
|
||||
preceding_codepoint = 0
|
||||
|
||||
func parse(data: Array, length: int):
|
||||
var code = 0
|
||||
var transition = 0
|
||||
var _current_state = current_state
|
||||
var dcs = _dcs_parser
|
||||
var collect = _collect
|
||||
var params = _params
|
||||
|
||||
#print("table", table)
|
||||
|
||||
#print("parse -> data: ", data, " length: ", length)
|
||||
|
||||
# Process input string.
|
||||
var i = 0
|
||||
while i < length:
|
||||
#print("i: ", i)
|
||||
code = data[i]
|
||||
|
||||
#print("code: ", code)
|
||||
|
||||
# Normal transition and action lookup.
|
||||
transition = _transitions[_current_state << TableAccess.INDEX_STATE_SHIFT | code if code < 0xa0 else NON_ASCII_PRINTABLE]
|
||||
|
||||
#print ("transition: ", transition)
|
||||
#print("current state: ", current_state)
|
||||
|
||||
match transition >> TableAccess.TRANSITION_ACTION_SHIFT:
|
||||
ParserAction.PRINT:
|
||||
# read ahead with loop unrolling
|
||||
# # Note: 0x20 (SP) is included, 0x7F (DEL) is excluded
|
||||
var j = i + 1
|
||||
while true:
|
||||
code = data[j] if j < data.size() else 0
|
||||
if j >= length or code < 0x20 or (code > 0x7e and code < NON_ASCII_PRINTABLE):
|
||||
_print_handler['target'].call(_print_handler['method'], data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
j += 1
|
||||
code = data[j] if j < data.size() else 0
|
||||
if j >= length or code < 0x20 or (code > 0x7e and code < NON_ASCII_PRINTABLE):
|
||||
_print_handler['target'].call(_print_handler['method'], data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
j += 1
|
||||
code = data[j] if j < data.size() else 0
|
||||
if j >= length or code < 0x20 or (code > 0x7e and code < NON_ASCII_PRINTABLE):
|
||||
_print_handler['target'].call(_print_handler['method'], data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
j += 1
|
||||
code = data[j] if j < data.size() else 0
|
||||
if j >= length or code < 0x20 or (code > 0x7e and code < NON_ASCII_PRINTABLE):
|
||||
_print_handler['target'].call(_print_handler['method'], data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
j += 1
|
||||
ParserAction.EXECUTE:
|
||||
var handler = _execute_handlers.get(code)
|
||||
if handler:
|
||||
print("EXEC: ", handler['method'])
|
||||
handler['target'].call(handler['method'])
|
||||
elif _execute_handler_fb:
|
||||
_execute_handler_fb['target'].call(_execute_handler_fb['method'], code)
|
||||
preceding_codepoint = 0
|
||||
ParserAction.IGNORE:
|
||||
pass
|
||||
ParserAction.ERROR:
|
||||
print("Parser error!")
|
||||
|
||||
ParserAction.CSI_DISPATCH:
|
||||
# Trigger CSI Handler
|
||||
var handlers = _csi_handlers.get((collect << 8 | code), [])
|
||||
handlers.invert()
|
||||
for handler in handlers:
|
||||
print("CSI: ", handler['method'])
|
||||
# undefined or true means success and to stop bubbling
|
||||
if handler['target'].call(handler['method'], params):
|
||||
continue
|
||||
handlers.invert()
|
||||
if handlers.empty():
|
||||
_csi_handler_fb['target'].call(_csi_handler_fb['method'], collect << 8 | code, params.to_array())
|
||||
preceding_codepoint = 0
|
||||
|
||||
|
||||
ParserAction.PARAM:
|
||||
# Inner loop digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
|
||||
var do = true
|
||||
while do:
|
||||
match code:
|
||||
0x3b:
|
||||
params.add_param(0)
|
||||
0x3a:
|
||||
params.add_sub_param(-1)
|
||||
_:
|
||||
params.add_digit(code - 48)
|
||||
i += 1
|
||||
code = data[i] if i < data.size() else 0
|
||||
do = i < length and code > 0x2f and code < 0x3c
|
||||
i-=1
|
||||
|
||||
ParserAction.COLLECT:
|
||||
collect <<= 8
|
||||
collect |= code
|
||||
|
||||
ParserAction.ESC_DISPATCH:
|
||||
var handlers = _esc_handlers.get((collect << 8 | code), [])
|
||||
handlers.invert()
|
||||
for handler in handlers:
|
||||
# undefined or true means success and to stop bubbling
|
||||
print("ESC: ", handler['method'])
|
||||
if handler['arg']:
|
||||
if handler['target'].call(handler['method'], handler['arg']) != false:
|
||||
continue
|
||||
else:
|
||||
if handler['target'].call(handler['method']) != false:
|
||||
continue
|
||||
handlers.invert()
|
||||
if handlers.empty():
|
||||
_esc_handler_fb['target'].call(_esc_handler_fb['method'], collect << 8 | code)
|
||||
preceding_codepoint = 0
|
||||
|
||||
ParserAction.CLEAR:
|
||||
params.reset()
|
||||
params.add_param(0) # ZDM
|
||||
collect = 0
|
||||
|
||||
ParserAction.DCS_HOOK:
|
||||
dcs.hook(collect << 8 | code, params.to_array())
|
||||
|
||||
ParserAction.DCS_PUT:
|
||||
# inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
|
||||
# unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)
|
||||
for j in range(i + 1, length + 1):
|
||||
code = data[j]
|
||||
if code == 0x18 or code == 0x1a or code == 0x1b or (code > 0x7f and code < NON_ASCII_PRINTABLE):
|
||||
dcs.put(data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
break
|
||||
ParserAction.DCS_UNHOOK:
|
||||
_dcs_parser.unhook(code != 0x18 and code != 0x1a)
|
||||
if code == 0x1b:
|
||||
transition |= ParserState.ESCAPE
|
||||
params.reset()
|
||||
params.add_param(0) # ZDM
|
||||
collect = 0;
|
||||
preceding_codepoint = 0
|
||||
ParserAction.OSC_START:
|
||||
pass
|
||||
|
||||
ParserAction.OSC_PUT:
|
||||
pass
|
||||
|
||||
ParserAction.OSC_END:
|
||||
pass
|
||||
|
||||
_current_state = transition & TableAccess.TRANSITION_STATE_MASK
|
||||
i += 1
|
||||
|
||||
# save collected intermediates
|
||||
_collect = collect
|
||||
|
||||
# save state
|
||||
current_state = _current_state
|
|
@ -1,136 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
# Max value supported for a single param/subparam (clamped to positive int32 range).
|
||||
const MAX_VALUE = 0x7FFFFFFF;
|
||||
# Max allowed subparams for a single sequence (hardcoded limitation).
|
||||
const MAX_SUBPARAMS = 256;
|
||||
|
||||
var params = []
|
||||
var length = 0
|
||||
|
||||
var sub_params = []
|
||||
var sub_params_length = 0
|
||||
var _max_length
|
||||
var _max_sub_params_length
|
||||
var sub_params_idx = []
|
||||
var _reject_digits = false
|
||||
var _reject_sub_digits = false
|
||||
var digit_is_sub = false
|
||||
|
||||
|
||||
static func from_array(values: Array):
|
||||
# Workaround as per: https://github.com/godotengine/godot/issues/19345#issuecomment-471218401
|
||||
var params = load("res://addons/godot_xterm/parser/params.gd").new()
|
||||
if values.empty():
|
||||
return params
|
||||
# skip leading sub params
|
||||
for i in range(values.size()):
|
||||
var value = values[i]
|
||||
if typeof(value) == TYPE_ARRAY:
|
||||
if i == 0:
|
||||
# skip leading sub params
|
||||
continue
|
||||
else:
|
||||
for sub_param in value:
|
||||
params.add_sub_param(sub_param)
|
||||
else:
|
||||
params.add_param(value)
|
||||
return params
|
||||
|
||||
|
||||
func _init(max_length: int = 32, max_sub_params_length: int = 32):
|
||||
_max_length = max_length
|
||||
_max_sub_params_length = max_sub_params_length
|
||||
|
||||
if (max_sub_params_length > MAX_SUBPARAMS):
|
||||
push_error("max_sub_params_length must not be greater than 256")
|
||||
|
||||
params.resize(max_length)
|
||||
sub_params.resize(max_sub_params_length)
|
||||
sub_params_idx.resize(max_length)
|
||||
|
||||
|
||||
# Gets param at `index` from param if it exists and is non-zero.
|
||||
# Otherwise returns `default` (which is zero anyway due to zero default
|
||||
# mode (ZDM), but allows the caller to specify a non-zero default value).
|
||||
func get_param(index: int, default = 0) -> int:
|
||||
if index < params.size() and params[index]:
|
||||
return params[index]
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
func add_param(value: int):
|
||||
digit_is_sub = false
|
||||
if length >= _max_length:
|
||||
_reject_digits = true
|
||||
return
|
||||
if value < -1:
|
||||
push_error('values lesser than -1 are not allowed')
|
||||
sub_params_idx[length] = sub_params_length << 8 | sub_params_length
|
||||
params[length] = MAX_VALUE if value > MAX_VALUE else value
|
||||
length += 1
|
||||
|
||||
|
||||
# Add a sub parameter value.
|
||||
# The sub parameter is automatically associated with the last parameter value.
|
||||
# Thus it is not possible to add a subparameter without any parameter added yet.
|
||||
# `Params` only stores up to `subParamsLength` sub parameters, any later
|
||||
# sub parameter will be ignored.
|
||||
func add_sub_param(value: int):
|
||||
digit_is_sub = true
|
||||
if !length:
|
||||
return
|
||||
if _reject_digits or sub_params_length >= _max_sub_params_length:
|
||||
_reject_sub_digits = true
|
||||
return
|
||||
if value < -1:
|
||||
push_error('values lesser than -1 are not allowed')
|
||||
sub_params[sub_params_length] = MAX_VALUE if value > MAX_VALUE else value
|
||||
sub_params_length += 1
|
||||
sub_params_idx[length - 1] += 1
|
||||
|
||||
|
||||
# Whether parameter at index `idx` has sub parameters.
|
||||
func has_sub_params(idx: int) -> bool:
|
||||
return (sub_params_idx[idx] & 0xFF) - (sub_params_idx[idx] >> 8) > 0
|
||||
|
||||
|
||||
func get_sub_params(idx: int):
|
||||
var start = sub_params_idx[idx] >> 8
|
||||
var end = sub_params_idx[idx] & 0xFF
|
||||
if end - start > 0:
|
||||
return sub_params.slice(start, end - 1)
|
||||
else:
|
||||
return null
|
||||
|
||||
|
||||
func add_digit(value: int):
|
||||
var _length = sub_params_length if digit_is_sub else length
|
||||
if _reject_digits or (not _length) or (digit_is_sub and _reject_sub_digits):
|
||||
return
|
||||
var store = sub_params if digit_is_sub else params
|
||||
var cur = store[_length - 1]
|
||||
store[_length - 1] = min(cur * 10 + value, MAX_VALUE) if ~cur else value
|
||||
|
||||
|
||||
func to_array():
|
||||
var res = []
|
||||
for i in range(length):
|
||||
res.append(params[i])
|
||||
var start = sub_params_idx[i] >> 8
|
||||
var end = sub_params_idx[i] & 0xff
|
||||
if end - start > 0:
|
||||
res.append(sub_params.slice(start, end - 1))
|
||||
return res
|
||||
|
||||
func reset():
|
||||
length = 0
|
||||
sub_params_length = 0
|
||||
_reject_digits = false
|
||||
_reject_sub_digits = false
|
||||
digit_is_sub = false
|
|
@ -1,26 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
enum TableAccess {
|
||||
TRANSITION_ACTION_SHIFT = 4,
|
||||
TRANSITION_STATE_MASK = 15,
|
||||
INDEX_STATE_SHIFT = 8
|
||||
}
|
||||
|
||||
var table: PoolByteArray = PoolByteArray()
|
||||
|
||||
func _init(length: int):
|
||||
table.resize(length)
|
||||
|
||||
func setDefault(action: int, next: int):
|
||||
for i in range(table.size()):
|
||||
table[i] = action << TableAccess.TRANSITION_ACTION_SHIFT | next
|
||||
|
||||
func add(code: int, state: int, action: int, next: int):
|
||||
table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next
|
||||
|
||||
func addMany(codes: Array, state: int, action: int, next: int):
|
||||
for code in codes:
|
||||
add(code, state, action, next)
|
|
@ -1,123 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends "res://addons/godot_xterm/parser/transition_table.gd"
|
||||
|
||||
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
|
||||
const ParserState = Constants.ParserState
|
||||
const ParserAction = Constants.ParserAction
|
||||
const NON_ASCII_PRINTABLE = Constants.NON_ASCII_PRINTABLE
|
||||
|
||||
var PRINTABLES = Array(range(0x20, 0x7f)) # 0x20 (SP) included, 0x7f (DEL) excluded.
|
||||
var EXECUTABLES = Array(range(0x00, 0x18)) + [0x19] + Array(range(0x1c, 0x20))
|
||||
|
||||
func _init().(4096):
|
||||
# Set default transition.
|
||||
setDefault(ParserAction.ERROR, ParserState.GROUND)
|
||||
|
||||
# Printables.
|
||||
addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND)
|
||||
|
||||
# Global anywhere rules.
|
||||
for state in ParserState.values():
|
||||
addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND)
|
||||
addMany(range(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND)
|
||||
addMany(range(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND)
|
||||
add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND) # ST as terminator
|
||||
add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE) # ESC
|
||||
add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING) # OSC
|
||||
addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY) # CSI
|
||||
add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY) # DCS
|
||||
|
||||
# Rules for executables and 7f.
|
||||
addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND)
|
||||
addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE)
|
||||
add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE)
|
||||
addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING)
|
||||
addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY)
|
||||
add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY)
|
||||
addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM)
|
||||
add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);
|
||||
addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE)
|
||||
addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE)
|
||||
add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE)
|
||||
addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE)
|
||||
add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);
|
||||
|
||||
# OSC.
|
||||
add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING)
|
||||
addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING)
|
||||
add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING)
|
||||
addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND)
|
||||
addMany(range(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING)
|
||||
|
||||
# SOS/PM/APC does nothing.
|
||||
addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND)
|
||||
add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
# csi entries
|
||||
add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY)
|
||||
addMany(range(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM)
|
||||
addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM)
|
||||
addMany(range(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM)
|
||||
addMany(range(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND)
|
||||
addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
addMany(range(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
addMany(range(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND)
|
||||
addMany(range(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE)
|
||||
addMany(range(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE)
|
||||
addMany(range(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
addMany(range(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE)
|
||||
# esc_intermediate
|
||||
addMany(range(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE)
|
||||
addMany(range(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE)
|
||||
addMany(range(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
|
||||
addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
|
||||
# dcs entry
|
||||
add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY)
|
||||
addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY)
|
||||
add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY)
|
||||
addMany(range(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY)
|
||||
addMany(range(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(range(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM)
|
||||
addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM)
|
||||
addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(range(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(range(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM)
|
||||
add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM)
|
||||
addMany(range(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM)
|
||||
addMany(range(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM)
|
||||
addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(range(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE)
|
||||
add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(range(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(range(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(range(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(range(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH)
|
||||
addMany(range(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH)
|
||||
addMany(range(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH)
|
||||
addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH)
|
||||
addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH)
|
||||
add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH)
|
||||
addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
|
||||
|
||||
# special handling of unicode chars
|
||||
add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND)
|
||||
add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING)
|
||||
add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH)
|
||||
|
||||
return table
|
|
@ -1,7 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="GodotXterm"
|
||||
description="Xterm.js for Godot"
|
||||
author="Leroy Hopson"
|
||||
description=""
|
||||
author="The GodotXterm authors"
|
||||
version="0.1.0"
|
||||
script="plugin.gd"
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
|
||||
# License MIT
|
||||
tool
|
||||
extends EditorPlugin
|
||||
|
||||
|
||||
func _enter_tree():
|
||||
var script = preload("res://addons/godot_xterm/terminal.gd")
|
||||
var texture = preload("res://addons/godot_xterm/icon.svg")
|
||||
add_custom_type("Terminal", "Control", script, texture)
|
||||
pass
|
||||
var terminal_script = preload("res://addons/godot_xterm/terminal.gdns")
|
||||
var terminal_icon = preload("res://addons/godot_xterm/terminal_icon.svg")
|
||||
add_custom_type("Terminal", "Control", terminal_script, terminal_icon)
|
||||
|
||||
var pseudoterminal_script = preload("res://addons/godot_xterm/pseudoterminal.gdns")
|
||||
var pseudoterminal_icon = preload("res://addons/godot_xterm/pseudoterminal_icon.svg")
|
||||
add_custom_type("Pseudoterminal", "Node", pseudoterminal_script, pseudoterminal_icon)
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
remove_custom_type("Terminal")
|
||||
pass
|
||||
remove_custom_type("Psuedoterminal")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[gd_resource type="NativeScript" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm_native/godotxtermnative.gdnlib" type="GDNativeLibrary" id=1]
|
||||
[ext_resource path="res://addons/godot_xterm/godotxtermnative.gdnlib" type="GDNativeLibrary" id=1]
|
||||
|
||||
[resource]
|
||||
resource_name = "Terminal"
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -2,15 +2,15 @@
|
|||
|
||||
importer="texture"
|
||||
type="StreamTexture"
|
||||
path="res://.import/icon.svg-b0b594198f5f4040e8f0e39a8a353265.stex"
|
||||
path="res://.import/pseudoterminal_icon.svg-50ba2514dae785a6b48b0da604cf3a09.stex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot_xterm/icon.svg"
|
||||
dest_files=[ "res://.import/icon.svg-b0b594198f5f4040e8f0e39a8a353265.stex" ]
|
||||
source_file="res://addons/godot_xterm/pseudoterminal_icon.svg"
|
||||
dest_files=[ "res://.import/pseudoterminal_icon.svg-50ba2514dae785a6b48b0da604cf3a09.stex" ]
|
||||
|
||||
[params]
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Node2D
|
||||
class_name CanvasRenderingContext2D
|
||||
# This is a shim for the CavasRenderingContext2D interface of HTML5's Canvas API,
|
||||
# which the xterm.js renderer code uses heavily. It extends Node2D to take
|
||||
# advantage of the z_index property and also uses many methods of CanvasItem
|
||||
# which Node2D inherits.
|
||||
|
||||
|
||||
var fill_style
|
||||
var font = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres")
|
||||
var _saved
|
||||
var _draw_buffer = []
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
pass # Replace with function body.
|
||||
|
||||
|
||||
func draw_rect_deferred(rect: Rect2, color: Color):
|
||||
_draw_buffer.append({"method": "draw_rect", "args": [rect, color]})
|
||||
update()
|
||||
|
||||
|
||||
func clear_rect(rect: Rect2):
|
||||
draw_rect_deferred(rect, Color(0, 0, 0, 0))
|
||||
|
||||
|
||||
func fill_rect(rect: Rect2):
|
||||
draw_rect_deferred(rect, fill_style)
|
||||
|
||||
|
||||
func fill_text(text: String, x: int, y: int):
|
||||
_draw_buffer.append({"method": "_draw_text", "args": [font, Vector2(x, y), text, fill_style]})
|
||||
update()
|
||||
|
||||
func _draw_text(font: Font, pos: Vector2, text: String, color) -> void:
|
||||
for i in text.length():
|
||||
var c = text[i]
|
||||
var next_char = text[i + 1] if i + 1 < text.length() else ''
|
||||
var advance = draw_char(font, pos, c, next_char, color)
|
||||
pos.x += advance
|
||||
|
||||
|
||||
func _draw():
|
||||
for command in _draw_buffer:
|
||||
self.callv(command.method, command.args)
|
||||
_draw_buffer.resize(0)
|
||||
|
||||
|
||||
func save():
|
||||
_saved = {
|
||||
'fill_style': fill_style,
|
||||
'font': font,
|
||||
}
|
||||
|
||||
|
||||
func restore():
|
||||
fill_style = _saved['fill_style']
|
||||
font = _saved['font']
|
||||
|
||||
|
||||
func measure_text(text: String):
|
||||
var text_metrics = TextMetrics.new()
|
||||
text_metrics.width = font.get_string_size(text).x
|
||||
return text_metrics
|
||||
|
||||
class TextMetrics:
|
||||
extends Reference
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics
|
||||
|
||||
var width
|
|
@ -1,46 +0,0 @@
|
|||
# Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
|
||||
|
||||
|
||||
class JoinedCellData extends "res://addons/godot_xterm/buffer/attribute_data.gd":
|
||||
|
||||
|
||||
var _width: int = 0
|
||||
var content: int = 0
|
||||
var combined_data: String = ''
|
||||
|
||||
|
||||
func _init(first_cell, chars: String, width: int):
|
||||
fg = first_cell.fg
|
||||
bg = first_cell.bg
|
||||
combined_data = chars
|
||||
_width = width
|
||||
|
||||
|
||||
var _character_joiners: Array = []
|
||||
var _next_character_joiner_id = 0
|
||||
var _work_cell = CellData.new()
|
||||
var _buffer_service
|
||||
|
||||
|
||||
func _init(buffer_service):
|
||||
_buffer_service = buffer_service
|
||||
|
||||
|
||||
func get_joined_characters(row: int) -> Array:
|
||||
if _character_joiners.empty():
|
||||
return []
|
||||
|
||||
var line = _buffer_service.buffer.lines.get_el(row)
|
||||
if not line or line.length == 0:
|
||||
return []
|
||||
|
||||
var ranges = []
|
||||
var line_str = line.translate_to_string(true)
|
||||
|
||||
return ranges
|
|
@ -1,53 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
signal buffer_activated(active_buffer, inactive_buffer)
|
||||
signal resized(cols, rows)
|
||||
|
||||
const BufferSet = preload("res://addons/godot_xterm/buffer/buffer_set.gd")
|
||||
|
||||
const MINIMUM_COLS = 2 # Less than 2 can mess with wide chars
|
||||
const MINIMUM_ROWS = 1
|
||||
|
||||
var service_brand
|
||||
|
||||
var cols: int
|
||||
var rows: int
|
||||
var buffers
|
||||
# Whether the user is scrolling (locks the scroll position)
|
||||
var is_user_scrolling: bool = false
|
||||
var _options_service
|
||||
|
||||
var buffer setget ,_get_buffer
|
||||
|
||||
|
||||
func _get_buffer():
|
||||
return buffers.active if buffers else null
|
||||
|
||||
|
||||
func _init(options_service):
|
||||
_options_service = options_service
|
||||
_options_service.connect("option_changed", self, "_option_changed")
|
||||
cols = max(_options_service.options.cols, MINIMUM_COLS)
|
||||
rows = max(_options_service.options.rows, MINIMUM_ROWS)
|
||||
buffers = BufferSet.new(_options_service, self)
|
||||
buffers.connect("buffer_activated", self, "_buffer_activated")
|
||||
|
||||
|
||||
func resize(cols: int, rows: int) -> void:
|
||||
self.cols = cols
|
||||
self.rows = rows
|
||||
buffers.resize(cols, rows)
|
||||
#buffers.setup_tab_stops(cols)
|
||||
emit_signal("resized", cols, rows)
|
||||
|
||||
|
||||
func _buffer_activated(active_buffer, inactive_buffer):
|
||||
emit_signal("buffer_activated", active_buffer, inactive_buffer)
|
||||
|
||||
|
||||
func _option_changed(option: String) -> void:
|
||||
if option == "cols" or option == "rows":
|
||||
resize(_options_service.options.cols, _options_service.options.rows)
|
|
@ -1,30 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
var service_brand
|
||||
var charset = null
|
||||
var glevel: int = 0
|
||||
|
||||
var _charsets = []
|
||||
|
||||
|
||||
func reset() -> void:
|
||||
charset = null
|
||||
_charsets = []
|
||||
glevel = 0
|
||||
|
||||
|
||||
func set_glevel(g: int) -> void:
|
||||
glevel = g
|
||||
charset = _charsets[g]
|
||||
|
||||
|
||||
func set_gcharset(g: int, charset = null) -> void:
|
||||
if _charsets.size() < g + 1:
|
||||
_charsets.resize(g + 1)
|
||||
_charsets[g] = charset
|
||||
if glevel == g:
|
||||
charset = charset
|
|
@ -1,27 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
var DEFAULT_MODES = {
|
||||
"insert_mode": false,
|
||||
}
|
||||
|
||||
var DEFAULT_DEC_PRIVATE_MODES = {
|
||||
"application_cursor_keys": false,
|
||||
"application_keypad": false,
|
||||
"bracketed_paste_mode": false,
|
||||
"origin": false,
|
||||
"reverse_wraparound": false, # defaults: xterm -true, vt100 - false
|
||||
}
|
||||
|
||||
var modes = DEFAULT_MODES.duplicate()
|
||||
var dec_private_modes = DEFAULT_DEC_PRIVATE_MODES.duplicate()
|
||||
var is_cursor_hidden = false
|
||||
var is_cursor_initialized = true
|
||||
|
||||
|
||||
func reset():
|
||||
modes = DEFAULT_MODES.duplicate()
|
||||
dec_private_modes.duplicate()
|
|
@ -1,104 +0,0 @@
|
|||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
|
||||
|
||||
const CursorStyle = Constants.CursorStyle
|
||||
const UnderlineStyle = Constants.UnderlineStyle
|
||||
const BellStyle = Constants.BellStyle
|
||||
|
||||
|
||||
class TerminalOptions:
|
||||
extends Reference
|
||||
|
||||
|
||||
var cols: int = 80
|
||||
var rows: int = 24
|
||||
var cursor_blink: bool = false
|
||||
var cursor_style = CursorStyle.BLOCK
|
||||
# var cursor_width: int = 1
|
||||
# var bell_sound: AudioStream = null
|
||||
# var bell_style = BellStyle.NONE
|
||||
# var draw_bold_text_in_bright_colors: bool = true
|
||||
# var fast_scroll_modifier = "alt"
|
||||
# var fast_scroll_sensitivity: int = 5
|
||||
var font_family: Dictionary = {
|
||||
# TODO
|
||||
}
|
||||
var font_size: int = 15
|
||||
# var font_weight: String # TODO: Remove
|
||||
# var font_weight_bold: String # TODO: Remove
|
||||
var line_height: float = 1.0
|
||||
# var link_tooltip_hover_duration: int # TODO: Remove
|
||||
var letter_spacing: float = 0
|
||||
# var log_level # TODO: implement
|
||||
var scrollback: int = 1000
|
||||
# var scroll_sensitivity: int = 1
|
||||
var screen_reader_mode: bool = false
|
||||
# var mac_option_is_meta: bool = false
|
||||
# var mac_option_click_forces_selection: bool = false
|
||||
# var minimum_contrast_ratio: float = 1
|
||||
# var disable_stdin: bool = false
|
||||
# var allow_proposed_api: bool = true
|
||||
var allow_transparency: bool = false
|
||||
var tab_stop_width: int = 8
|
||||
# var colors: Dictionary = {
|
||||
# 'black': Color(0, 0, 0)
|
||||
# }
|
||||
# var right_click_selects_word = "isMac" # TODO?
|
||||
# var renderer_type = "canvas" # Remove?
|
||||
var window_options: Dictionary = {
|
||||
'set_win_lines': false,
|
||||
}
|
||||
var windows_mode: bool = false
|
||||
# var word_separator: String = " ()[]{}',\""
|
||||
var convert_eol: bool = true
|
||||
# var term_name: String = "xterm"
|
||||
# var cancel_events: bool = false
|
||||
|
||||
|
||||
# Copies options from an `object` to itself.
|
||||
func copy_from(object: Object):
|
||||
for property in get_property_list():
|
||||
if property.usage == PROPERTY_USAGE_SCRIPT_VARIABLE:
|
||||
var p = object.get(property.name)
|
||||
if p:
|
||||
set(property.name, p)
|
||||
|
||||
|
||||
var DEFAULT_OPTIONS = TerminalOptions.new()
|
||||
|
||||
signal option_changed
|
||||
|
||||
var options
|
||||
|
||||
|
||||
func _init(options):
|
||||
self.options = options
|
||||
|
||||
# Set the font size based on the font_size option
|
||||
_resize_fonts()
|
||||
|
||||
|
||||
func set_option(key: String, value) -> void:
|
||||
# TODO: sanitize and validate options.
|
||||
|
||||
# Don't fire an option change event if they didn't change
|
||||
if options[key] == value:
|
||||
return
|
||||
|
||||
options[key] = value
|
||||
emit_signal("option_changed", key)
|
||||
|
||||
# Update other options accordingly.
|
||||
match key:
|
||||
"font_size":
|
||||
_resize_fonts()
|
||||
|
||||
|
||||
func _resize_fonts():
|
||||
for font in options.font_family.values():
|
||||
font.size = options.font_size
|
|
@ -1,390 +0,0 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
|
||||
# Copyright (c) 2014-2020 The xterm.js authors. All rights reserved.
|
||||
# Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
# Ported to GDScript by the GodotXterm authors.
|
||||
# Licese MIT
|
||||
#
|
||||
# Originally forked from (with the author's permission):
|
||||
# Fabrice Bellard's javascript vt100 for jslinux:
|
||||
# http://bellard.org/jslinux/
|
||||
# Copyright (c) 2011 Fabrice Bellard
|
||||
# The original design remains. The terminal itself
|
||||
# has been extended to include xterm CSI codes, among
|
||||
# other features.
|
||||
#
|
||||
# Terminal Emulation References:
|
||||
# http://vt100.net/
|
||||
# http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt
|
||||
# http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
# http://invisible-island.net/vttest/
|
||||
# http://www.inwap.com/pdp10/ansicode.txt
|
||||
# http://linux.die.net/man/4/console_codes
|
||||
# http://linux.die.net/man/7/urxvt
|
||||
tool
|
||||
extends Control
|
||||
|
||||
|
||||
const BufferService = preload("res://addons/godot_xterm/services/buffer_service.gd")
|
||||
const CoreService = preload("res://addons/godot_xterm/services/core_service.gd")
|
||||
const OptionsService = preload("res://addons/godot_xterm/services/options_service.gd")
|
||||
const CharsetService = preload("res://addons/godot_xterm/services/charset_service.gd")
|
||||
const InputHandler = preload("res://addons/godot_xterm/input_handler.gd")
|
||||
const Const = preload("res://addons/godot_xterm/Constants.gd")
|
||||
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
|
||||
const Parser = preload("res://addons/godot_xterm/parser/escape_sequence_parser.gd")
|
||||
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
|
||||
const ColorManager = preload("res://addons/godot_xterm/color_manager.gd")
|
||||
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
|
||||
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
|
||||
|
||||
const SourceCodeProRegular = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres")
|
||||
const SourceCodeProBold = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres")
|
||||
const SourceCodeProItalic = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_italic.tres")
|
||||
const SourceCodeProBoldItalic = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold_italic.tres")
|
||||
|
||||
const C0 = Constants.C0
|
||||
const C1 = Constants.C1
|
||||
const ESCAPE = 27
|
||||
const BACKSPACE = 8
|
||||
const BEEP = 7
|
||||
const SPACE = 32
|
||||
const LEFT_BRACKET = 91
|
||||
const ENTER = 10
|
||||
const BACKSPACE_ALT = 127
|
||||
|
||||
const BLINK_INTERVAL = 0.6 # 600ms. The time between blinks.
|
||||
|
||||
# TODO: Move me somewhere else.
|
||||
enum BellStyle {
|
||||
NONE
|
||||
}
|
||||
|
||||
signal output(data)
|
||||
signal scrolled(ydisp)
|
||||
|
||||
export var cols = 80
|
||||
export var rows = 24
|
||||
# If set, terminals rows and cols will be automatically calculated based on the
|
||||
# control's rect size and font_size.
|
||||
export var auto_resize = false
|
||||
export var cursor_blink = false
|
||||
export var cursor_style = 'block'
|
||||
export var cursor_width = 1
|
||||
export var bell_sound: AudioStream = null # TODO Bell sound
|
||||
export(BellStyle) var bell_style = BellStyle.NONE
|
||||
export var draw_bold_text_in_bright_colors = true
|
||||
export var fast_scroll_modifier = 'alt' # TODO Use scancode?
|
||||
export var fast_scroll_sensitivity = 5
|
||||
export var font_family: Dictionary = {
|
||||
"regular": SourceCodeProRegular,
|
||||
"bold": SourceCodeProBold,
|
||||
"italic": SourceCodeProItalic,
|
||||
"bold_italic": SourceCodeProBoldItalic,
|
||||
}
|
||||
export var font_size: int = 15
|
||||
export var line_height = 1.0
|
||||
export var link_tooltip_hover_duration = 500 # Not relevant?
|
||||
export var letter_spacing = 0
|
||||
export var log_level = 'info' # Not relevant?
|
||||
export var scrollback = 1000
|
||||
export var scroll_sensitivity = 1
|
||||
export var screen_reader_mode: bool = false
|
||||
export var mac_option_is_meta = false
|
||||
export var mac_option_click_forces_selection = false
|
||||
export var minimum_contrast_ratio = 1
|
||||
export var disable_stdin = false
|
||||
export var allow_proposed_api = true
|
||||
export var allow_transparency = false
|
||||
export var tab_stop_width = 8
|
||||
export var colors: Dictionary = {
|
||||
"black": Color("#2e3436"),
|
||||
"red": Color("#cc0000"),
|
||||
"green": Color("#4e9a06"),
|
||||
"yellow": Color("#c4a000"),
|
||||
"blue": Color("#3465a4"),
|
||||
"magenta": Color("#75507b"),
|
||||
"cyan": Color("#06989a"),
|
||||
"white": Color("#d3d7cf"),
|
||||
"bright_black": Color("#555753"),
|
||||
"bright_red": Color("#ef2929"),
|
||||
"bright_green": Color("#8ae234"),
|
||||
"bright_yellow": Color("#fce94f"),
|
||||
"bright_blue": Color("#729fcf"),
|
||||
"bright_magenta": Color("#ad7fa8"),
|
||||
"bright_cyan": Color("#34e2e2"),
|
||||
"bright_white": Color("#eeeeec"),
|
||||
}
|
||||
export var right_click_selects_word = 'isMac' # TODO
|
||||
export var renderer_type = 'canvas' # Relevant?
|
||||
export var window_options = {
|
||||
'set_win_lines': false
|
||||
}
|
||||
export var windows_mode = false
|
||||
export var word_separator = " ()[]{}',\"`"
|
||||
export var convert_eol = true
|
||||
export var term_name = 'xterm'
|
||||
export var cancel_events = false
|
||||
|
||||
var options_service
|
||||
var decoder
|
||||
var parser
|
||||
var _buffer_service
|
||||
var _core_service
|
||||
var _charset_service
|
||||
var _input_handler
|
||||
var _render_service
|
||||
var _color_manager
|
||||
var _scaled_char_width
|
||||
var _scaled_char_height
|
||||
var _scaled_cell_width
|
||||
var _scaled_cell_height
|
||||
var _scaled_char_top
|
||||
var _scaled_char_left
|
||||
var _work_cell = CellData.new()
|
||||
var _blink_on = false
|
||||
var _time_since_last_blink = 0
|
||||
|
||||
func _ready():
|
||||
var options = OptionsService.TerminalOptions.new()
|
||||
options.copy_from(self)
|
||||
options_service = OptionsService.new(options)
|
||||
|
||||
_buffer_service = BufferService.new(options_service)
|
||||
_core_service = CoreService.new()
|
||||
_charset_service = CharsetService.new()
|
||||
|
||||
|
||||
# Register input handler and connect signals.
|
||||
_input_handler = InputHandler.new(_buffer_service, _core_service, _charset_service, options_service)
|
||||
_input_handler.connect("bell_requested", self, "bell")
|
||||
_input_handler.connect("refresh_rows_requested", self, "_refresh_rows")
|
||||
_input_handler.connect("reset_requested", self, "reset")
|
||||
_input_handler.connect("scroll_requested", self, "scroll")
|
||||
_input_handler.connect("windows_options_report_requested", self, "report_windows_options")
|
||||
|
||||
_color_manager = ColorManager.new()
|
||||
_color_manager.set_theme(colors)
|
||||
|
||||
if auto_resize:
|
||||
connect("resized", self, "_update_dimensions")
|
||||
|
||||
_update_dimensions()
|
||||
|
||||
|
||||
|
||||
func _refresh_rows(start_row = 0, end_row = 0):
|
||||
# Not optimized, just draw
|
||||
update()
|
||||
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventKey and event.pressed:
|
||||
var data = PoolByteArray([])
|
||||
accept_event()
|
||||
|
||||
# TODO: Handle more of these.
|
||||
if (event.control and event.scancode == KEY_C):
|
||||
data.append(3)
|
||||
elif event.unicode:
|
||||
data.append(event.unicode)
|
||||
elif event.scancode == KEY_ENTER:
|
||||
data.append(ENTER)
|
||||
elif event.scancode == KEY_BACKSPACE:
|
||||
data.append(BACKSPACE_ALT)
|
||||
elif event.scancode == KEY_ESCAPE:
|
||||
data.append(27)
|
||||
elif event.scancode == KEY_TAB:
|
||||
data.append(9)
|
||||
elif OS.get_scancode_string(event.scancode) == "Shift":
|
||||
pass
|
||||
elif OS.get_scancode_string(event.scancode) == "Control":
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
#push_warning('Unhandled input. scancode: ' + str(OS.get_scancode_string(event.scancode)))
|
||||
emit_signal("output", data)
|
||||
|
||||
|
||||
func write(data, callback_target = null, callback_method: String = ''):
|
||||
_input_handler.parse(data)
|
||||
if callback_target and callback_method:
|
||||
callback_target.call(callback_method)
|
||||
|
||||
|
||||
func refresh(start = null, end = null) -> void:
|
||||
pass
|
||||
|
||||
|
||||
# Recalculates the character and canvas dimensions.
|
||||
func _update_dimensions():
|
||||
var char_width = 0
|
||||
var char_height = 0
|
||||
|
||||
for font in options_service.options.font_family.values():
|
||||
var size = font.get_string_size("W")
|
||||
char_width = max(char_width, size.x)
|
||||
char_height = max(char_height, size.y)
|
||||
|
||||
_scaled_char_width = char_width
|
||||
_scaled_char_height = char_height
|
||||
|
||||
# Calculate the scaled cell height, if line_height is not 1 then the value
|
||||
# will be floored because since line_height can never be lower then 1, there
|
||||
# is a guarantee that the scaled line height will always be larger than
|
||||
# scaled char height.
|
||||
_scaled_cell_height = floor(_scaled_char_height * options_service.options.line_height)
|
||||
|
||||
# Calculate the y coordinate within a cell that text should draw from in
|
||||
# order to draw in the center of a cell.
|
||||
_scaled_char_top = 0 if options_service.options.line_height == 1 else \
|
||||
round((_scaled_cell_height - _scaled_char_height) / 2)
|
||||
|
||||
# Calculate the scaled cell width, taking the letter_spacing into account.
|
||||
_scaled_cell_width = _scaled_char_width + round(options_service.options.letter_spacing)
|
||||
|
||||
# Calculate the x coordinate with a cell that text should draw from in
|
||||
# order to draw in the center of a cell.
|
||||
_scaled_char_left = floor(options_service.options.letter_spacing / 2)
|
||||
|
||||
if auto_resize:
|
||||
# Calculate cols and rows based on cell size
|
||||
var rect: Rect2 = get_rect()
|
||||
var cols = max(2, floor(rect.size.x / _scaled_cell_width))
|
||||
var rows = max(1, floor(rect.size.y / _scaled_cell_height))
|
||||
|
||||
self.cols = cols
|
||||
self.rows = rows
|
||||
|
||||
options_service.set_option("rows", rows)
|
||||
options_service.set_option("cols", cols)
|
||||
|
||||
|
||||
# Scroll the terminal down 1 row, creating a blank line.
|
||||
# @param is_wrapped Whether the new line is wrapped from the previous line.
|
||||
func scroll(erase_attr, is_wrapped: bool = false) -> void:
|
||||
var buffer = _buffer_service.buffer
|
||||
var new_line = buffer.get_blank_line(erase_attr, is_wrapped)
|
||||
|
||||
var top_row = buffer.ybase + buffer.scroll_top
|
||||
var bottom_row = buffer.ybase + buffer.scroll_bottom
|
||||
|
||||
if buffer.scroll_top == 0:
|
||||
# Determine whether the buffer is going to be trimmed after insertion.
|
||||
var will_buffer_be_trimmed = buffer.lines.is_full
|
||||
|
||||
# Insert the line using the fastest method
|
||||
if bottom_row == buffer.lines.length - 1:
|
||||
if will_buffer_be_trimmed:
|
||||
buffer.lines.recycle().copy_from(new_line.duplicate())
|
||||
else:
|
||||
buffer.lines.push(new_line.duplicate())
|
||||
else:
|
||||
buffer.lines.splice(bottom_row + 1, 0, [new_line.duplicate()])
|
||||
|
||||
# Only adjust ybase and ydisp when the buffer is not trimmed
|
||||
if not will_buffer_be_trimmed:
|
||||
buffer.ybase += 1
|
||||
# Only scroll the ydisp with ybase if the user has not scrolled up
|
||||
if not _buffer_service.is_user_scrolling:
|
||||
buffer.ydisp += 1
|
||||
else:
|
||||
# When the buffer is full and the user has scrolled up, keep the text
|
||||
# stable unless ydisp is right at the top
|
||||
if _buffer_service.is_user_scrolling:
|
||||
buffer.ydisp = max(buffer.ydisp - 1, 0)
|
||||
else:
|
||||
# scroll_top is non-zero which means no line will be going to the
|
||||
# scrollback, instead we can just shift them in-place.
|
||||
var scroll_region_height = bottom_row - top_row + 1 # as it's zero based
|
||||
buffer.lines.shift_elements(top_row + 1, scroll_region_height - 1, -1)
|
||||
buffer.lines.set_line(bottom_row, new_line.duplicate())
|
||||
|
||||
# Move the viewport to the bottom of the buffer unless the user is scrolling.
|
||||
if not _buffer_service.is_user_scrolling:
|
||||
buffer.ydisp = buffer.ybase
|
||||
|
||||
# Flag rows that need updating
|
||||
# TODO
|
||||
|
||||
emit_signal("scrolled", buffer.ydisp)
|
||||
|
||||
|
||||
func _process(delta):
|
||||
_time_since_last_blink += delta
|
||||
if _time_since_last_blink > BLINK_INTERVAL:
|
||||
_blink_on = not _blink_on
|
||||
_time_since_last_blink = 0
|
||||
update()
|
||||
|
||||
|
||||
func _draw():
|
||||
# Draw the background and foreground
|
||||
if _buffer_service == null:
|
||||
return
|
||||
|
||||
var buffer = _buffer_service.buffer
|
||||
var rows = _buffer_service.rows
|
||||
|
||||
for y in range(0, rows):
|
||||
var row = y + buffer.ydisp
|
||||
var line = buffer.lines.get_line(row)
|
||||
for x in line.length:
|
||||
line.load_cell(x, _work_cell)
|
||||
|
||||
# Background
|
||||
|
||||
# Get the background color
|
||||
# TODO: handle inverse
|
||||
var bg_color
|
||||
if _work_cell.is_bg_rgb():
|
||||
bg_color = AttributeData.to_color_rgb(_work_cell.get_bg_color())
|
||||
elif _work_cell.is_bg_palette():
|
||||
bg_color = _color_manager.colors.ansi[_work_cell.get_bg_color()]
|
||||
else:
|
||||
bg_color = _color_manager.colors.background
|
||||
|
||||
draw_rect(Rect2(x * _scaled_cell_width, y * _scaled_cell_height,
|
||||
(cols - x) * _scaled_cell_width, 1 * _scaled_cell_height),
|
||||
bg_color)
|
||||
|
||||
# Foreground
|
||||
# Don't draw if cell is invisible
|
||||
if _work_cell.is_invisible():
|
||||
continue
|
||||
|
||||
# Don't draw if cell is blink and blink is off
|
||||
if _work_cell.is_blink() and not _blink_on:
|
||||
continue
|
||||
|
||||
# Get the foreground color
|
||||
# TODO: handle inverse min contrast and draw bold in bright colors
|
||||
# dim and maybe more!
|
||||
var fg_color
|
||||
if _work_cell.is_fg_default():
|
||||
fg_color = _color_manager.colors.foreground
|
||||
if _work_cell.is_fg_rgb():
|
||||
fg_color = AttributeData.to_color_rgb(_work_cell.get_fg_color())
|
||||
else:
|
||||
fg_color = _color_manager.colors.ansi[_work_cell.get_fg_color()]
|
||||
|
||||
# Get font
|
||||
var font: DynamicFont = options_service.options.font_family.regular
|
||||
var is_bold = _work_cell.is_bold()
|
||||
var is_italic = _work_cell.is_italic()
|
||||
|
||||
if is_bold and is_italic:
|
||||
font = options_service.options.font_family.bold_italic
|
||||
elif is_bold:
|
||||
font = options_service.options.font_family.bold
|
||||
elif is_italic:
|
||||
font = options_service.options.font_family.italic
|
||||
|
||||
# TODO: set this once initially
|
||||
font.size = options_service.options.font_size
|
||||
|
||||
draw_char(font,
|
||||
Vector2(x * _scaled_cell_width + _scaled_char_left,
|
||||
y * _scaled_cell_height + _scaled_char_top + _scaled_char_height / 2),
|
||||
_work_cell.get_chars() if _work_cell.get_chars() else ' ', "", fg_color)
|
||||
# Draw the cursor
|
||||
# Draw selection
|
|
@ -1,6 +1,6 @@
|
|||
[gd_resource type="NativeScript" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm_native/godotxtermnative.gdnlib" type="GDNativeLibrary" id=1]
|
||||
[ext_resource path="res://addons/godot_xterm/godotxtermnative.gdnlib" type="GDNativeLibrary" id=1]
|
||||
|
||||
[resource]
|
||||
resource_name = "Terminal"
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -2,15 +2,15 @@
|
|||
|
||||
importer="texture"
|
||||
type="StreamTexture"
|
||||
path="res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex"
|
||||
path="res://.import/terminal_icon.svg-33ee6ad8b86db2f37e5d8d61a6b1b8db.stex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/gut/icon.png"
|
||||
dest_files=[ "res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex" ]
|
||||
source_file="res://addons/godot_xterm/terminal_icon.svg"
|
||||
dest_files=[ "res://.import/terminal_icon.svg-33ee6ad8b86db2f37e5d8d61a6b1b8db.stex" ]
|
||||
|
||||
[params]
|
||||
|
BIN
addons/godot_xterm/themes/default.theme
Normal file
BIN
addons/godot_xterm/themes/default.theme
Normal file
Binary file not shown.
|
@ -1,16 +0,0 @@
|
|||
[general]
|
||||
|
||||
singleton=false
|
||||
load_once=true
|
||||
symbol_prefix="godot_"
|
||||
reloadable=true
|
||||
|
||||
[entry]
|
||||
|
||||
X11.64="res://addons/godot_xterm_native/bin/x11/libgodotxtermnative.so"
|
||||
Server.64="res://addons/godot_xterm_native/bin/x11/libgodotxtermnative.so"
|
||||
|
||||
[dependencies]
|
||||
|
||||
X11.64=[ ]
|
||||
Server.64=[ ]
|
|
@ -1,7 +0,0 @@
|
|||
[plugin]
|
||||
|
||||
name="Godot Xterm Native"
|
||||
description=""
|
||||
author="Leroy Hopson"
|
||||
version="0.1"
|
||||
script="plugin.gd"
|
|
@ -1,17 +0,0 @@
|
|||
tool
|
||||
extends EditorPlugin
|
||||
|
||||
|
||||
func _enter_tree():
|
||||
var terminal_script = preload("res://addons/godot_xterm_native/terminal.gdns")
|
||||
var terminal_icon = preload("res://addons/godot_xterm_native/terminal_icon.svg")
|
||||
add_custom_type("Terminal", "Control", terminal_script, terminal_icon)
|
||||
|
||||
var pseudoterminal_script = preload("res://addons/godot_xterm_native/pseudoterminal.gdns")
|
||||
var pseudoterminal_icon = preload("res://addons/godot_xterm_native/pseudoterminal_icon.svg")
|
||||
add_custom_type("Pseudoterminal", "Node", pseudoterminal_script, pseudoterminal_icon)
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
remove_custom_type("Terminal")
|
||||
remove_custom_type("Psuedoterminal")
|
|
@ -1,34 +0,0 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="StreamTexture"
|
||||
path="res://.import/pseudoterminal_icon.svg-11c935c96560f4f752005a829ae0b2b7.stex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot_xterm_native/pseudoterminal_icon.svg"
|
||||
dest_files=[ "res://.import/pseudoterminal_icon.svg-11c935c96560f4f752005a829ae0b2b7.stex" ]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_mode=0
|
||||
compress/bptc_ldr=0
|
||||
compress/normal_map=0
|
||||
flags/repeat=0
|
||||
flags/filter=true
|
||||
flags/mipmaps=false
|
||||
flags/anisotropic=false
|
||||
flags/srgb=2
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/HDR_as_SRGB=false
|
||||
process/invert_color=false
|
||||
stream=false
|
||||
size_limit=0
|
||||
detect_3d=true
|
||||
svg/scale=1.0
|
|
@ -1,34 +0,0 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="StreamTexture"
|
||||
path="res://.import/terminal_icon.svg-ab79ba4c14608847c16f36c424b01cf4.stex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot_xterm_native/terminal_icon.svg"
|
||||
dest_files=[ "res://.import/terminal_icon.svg-ab79ba4c14608847c16f36c424b01cf4.stex" ]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_mode=0
|
||||
compress/bptc_ldr=0
|
||||
compress/normal_map=0
|
||||
flags/repeat=0
|
||||
flags/filter=true
|
||||
flags/mipmaps=false
|
||||
flags/anisotropic=false
|
||||
flags/srgb=2
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/HDR_as_SRGB=false
|
||||
process/invert_color=false
|
||||
stream=false
|
||||
size_limit=0
|
||||
detect_3d=true
|
||||
svg/scale=1.0
|
Binary file not shown.
|
@ -1,347 +0,0 @@
|
|||
extends Panel
|
||||
|
||||
onready var _script_list = $ScriptsList
|
||||
onready var _nav = {
|
||||
prev = $Navigation/Previous,
|
||||
next = $Navigation/Next,
|
||||
run = $Navigation/Run,
|
||||
current_script = $Navigation/CurrentScript,
|
||||
show_scripts = $Navigation/ShowScripts
|
||||
}
|
||||
onready var _progress = {
|
||||
script = $ScriptProgress,
|
||||
test = $TestProgress
|
||||
}
|
||||
onready var _summary = {
|
||||
failing = $Summary/Failing,
|
||||
passing = $Summary/Passing
|
||||
}
|
||||
|
||||
onready var _extras = $ExtraOptions
|
||||
onready var _ignore_pauses = $ExtraOptions/IgnorePause
|
||||
onready var _continue_button = $Continue/Continue
|
||||
onready var _text_box = $TextDisplay/RichTextLabel
|
||||
|
||||
onready var _titlebar = {
|
||||
bar = $TitleBar,
|
||||
time = $TitleBar/Time,
|
||||
label = $TitleBar/Title
|
||||
}
|
||||
|
||||
var _mouse = {
|
||||
down = false,
|
||||
in_title = false,
|
||||
down_pos = null,
|
||||
in_handle = false
|
||||
}
|
||||
var _is_running = false
|
||||
var _start_time = 0.0
|
||||
var _time = 0.0
|
||||
|
||||
const DEFAULT_TITLE = 'Gut: The Godot Unit Testing tool.'
|
||||
var _utils = load('res://addons/gut/utils.gd').new()
|
||||
var _text_box_blocker_enabled = true
|
||||
var _pre_maximize_size = null
|
||||
|
||||
signal end_pause
|
||||
signal ignore_pause
|
||||
signal log_level_changed
|
||||
signal run_script
|
||||
signal run_single_script
|
||||
|
||||
func _ready():
|
||||
_pre_maximize_size = rect_size
|
||||
_hide_scripts()
|
||||
_update_controls()
|
||||
_nav.current_script.set_text("No scripts available")
|
||||
set_title()
|
||||
clear_summary()
|
||||
$TitleBar/Time.set_text("")
|
||||
$ExtraOptions/DisableBlocker.pressed = !_text_box_blocker_enabled
|
||||
_extras.visible = false
|
||||
update()
|
||||
|
||||
func _process(_delta):
|
||||
if(_is_running):
|
||||
_time = OS.get_unix_time() - _start_time
|
||||
var disp_time = round(_time * 100)/100
|
||||
$TitleBar/Time.set_text(str(disp_time))
|
||||
|
||||
func _draw(): # needs get_size()
|
||||
# Draw the lines in the corner to show where you can
|
||||
# drag to resize the dialog
|
||||
var grab_margin = 3
|
||||
var line_space = 3
|
||||
var grab_line_color = Color(.4, .4, .4)
|
||||
for i in range(1, 10):
|
||||
var x = rect_size - Vector2(i * line_space, grab_margin)
|
||||
var y = rect_size - Vector2(grab_margin, i * line_space)
|
||||
draw_line(x, y, grab_line_color, 1, true)
|
||||
|
||||
func _on_Maximize_draw():
|
||||
# draw the maximize square thing.
|
||||
var btn = $TitleBar/Maximize
|
||||
btn.set_text('')
|
||||
var w = btn.get_size().x
|
||||
var h = btn.get_size().y
|
||||
btn.draw_rect(Rect2(0, 0, w, h), Color(0, 0, 0, 1))
|
||||
btn.draw_rect(Rect2(2, 4, w - 4, h - 6), Color(1,1,1,1))
|
||||
|
||||
func _on_ShowExtras_draw():
|
||||
var btn = $Continue/ShowExtras
|
||||
btn.set_text('')
|
||||
var start_x = 20
|
||||
var start_y = 15
|
||||
var pad = 5
|
||||
var color = Color(.1, .1, .1, 1)
|
||||
var width = 2
|
||||
for i in range(3):
|
||||
var y = start_y + pad * i
|
||||
btn.draw_line(Vector2(start_x, y), Vector2(btn.get_size().x - start_x, y), color, width, true)
|
||||
|
||||
# ####################
|
||||
# GUI Events
|
||||
# ####################
|
||||
func _on_Run_pressed():
|
||||
_run_mode()
|
||||
emit_signal('run_script', get_selected_index())
|
||||
|
||||
func _on_CurrentScript_pressed():
|
||||
_run_mode()
|
||||
emit_signal('run_single_script', get_selected_index())
|
||||
|
||||
func _on_Previous_pressed():
|
||||
_select_script(get_selected_index() - 1)
|
||||
|
||||
func _on_Next_pressed():
|
||||
_select_script(get_selected_index() + 1)
|
||||
|
||||
func _on_LogLevelSlider_value_changed(_value):
|
||||
emit_signal('log_level_changed', $LogLevelSlider.value)
|
||||
|
||||
func _on_Continue_pressed():
|
||||
_continue_button.disabled = true
|
||||
emit_signal('end_pause')
|
||||
|
||||
func _on_IgnorePause_pressed():
|
||||
var checked = _ignore_pauses.is_pressed()
|
||||
emit_signal('ignore_pause', checked)
|
||||
if(checked):
|
||||
emit_signal('end_pause')
|
||||
_continue_button.disabled = true
|
||||
|
||||
func _on_ShowScripts_pressed():
|
||||
_toggle_scripts()
|
||||
|
||||
func _on_ScriptsList_item_selected(index):
|
||||
_select_script(index)
|
||||
|
||||
func _on_TitleBar_mouse_entered():
|
||||
_mouse.in_title = true
|
||||
|
||||
func _on_TitleBar_mouse_exited():
|
||||
_mouse.in_title = false
|
||||
|
||||
func _input(event):
|
||||
if(event is InputEventMouseButton):
|
||||
if(event.button_index == 1):
|
||||
_mouse.down = event.pressed
|
||||
if(_mouse.down):
|
||||
_mouse.down_pos = event.position
|
||||
|
||||
if(_mouse.in_title):
|
||||
if(event is InputEventMouseMotion and _mouse.down):
|
||||
set_position(get_position() + (event.position - _mouse.down_pos))
|
||||
_mouse.down_pos = event.position
|
||||
|
||||
if(_mouse.in_handle):
|
||||
if(event is InputEventMouseMotion and _mouse.down):
|
||||
var new_size = rect_size + event.position - _mouse.down_pos
|
||||
var new_mouse_down_pos = event.position
|
||||
rect_size = new_size
|
||||
_mouse.down_pos = new_mouse_down_pos
|
||||
_pre_maximize_size = rect_size
|
||||
|
||||
func _on_ResizeHandle_mouse_entered():
|
||||
_mouse.in_handle = true
|
||||
|
||||
func _on_ResizeHandle_mouse_exited():
|
||||
_mouse.in_handle = false
|
||||
|
||||
# Send scroll type events through to the text box
|
||||
func _on_FocusBlocker_gui_input(ev):
|
||||
if(_text_box_blocker_enabled):
|
||||
if(ev is InputEventPanGesture):
|
||||
get_text_box()._gui_input(ev)
|
||||
# convert a drag into a pan gesture so it scrolls.
|
||||
elif(ev is InputEventScreenDrag):
|
||||
var converted = InputEventPanGesture.new()
|
||||
converted.delta = Vector2(0, ev.relative.y)
|
||||
converted.position = Vector2(0, 0)
|
||||
get_text_box()._gui_input(converted)
|
||||
elif(ev is InputEventMouseButton and (ev.button_index == BUTTON_WHEEL_DOWN or ev.button_index == BUTTON_WHEEL_UP)):
|
||||
get_text_box()._gui_input(ev)
|
||||
else:
|
||||
get_text_box()._gui_input(ev)
|
||||
print(ev)
|
||||
|
||||
func _on_RichTextLabel_gui_input(ev):
|
||||
pass
|
||||
# leaving this b/c it is wired up and might have to send
|
||||
# more signals through
|
||||
print(ev)
|
||||
|
||||
func _on_Copy_pressed():
|
||||
_text_box.select_all()
|
||||
_text_box.copy()
|
||||
_text_box.deselect()
|
||||
|
||||
func _on_DisableBlocker_toggled(button_pressed):
|
||||
_text_box_blocker_enabled = !button_pressed
|
||||
|
||||
func _on_ShowExtras_toggled(button_pressed):
|
||||
_extras.visible = button_pressed
|
||||
|
||||
func _on_Maximize_pressed():
|
||||
if(rect_size == _pre_maximize_size):
|
||||
maximize()
|
||||
else:
|
||||
rect_size = _pre_maximize_size
|
||||
# ####################
|
||||
# Private
|
||||
# ####################
|
||||
func _run_mode(is_running=true):
|
||||
if(is_running):
|
||||
_start_time = OS.get_unix_time()
|
||||
_time = _start_time
|
||||
_summary.failing.set_text("0")
|
||||
_summary.passing.set_text("0")
|
||||
_is_running = is_running
|
||||
|
||||
_hide_scripts()
|
||||
var ctrls = $Navigation.get_children()
|
||||
for i in range(ctrls.size()):
|
||||
ctrls[i].disabled = is_running
|
||||
|
||||
func _select_script(index):
|
||||
$Navigation/CurrentScript.set_text(_script_list.get_item_text(index))
|
||||
_script_list.select(index)
|
||||
_update_controls()
|
||||
|
||||
func _toggle_scripts():
|
||||
if(_script_list.visible):
|
||||
_hide_scripts()
|
||||
else:
|
||||
_show_scripts()
|
||||
|
||||
func _show_scripts():
|
||||
_script_list.show()
|
||||
|
||||
func _hide_scripts():
|
||||
_script_list.hide()
|
||||
|
||||
func _update_controls():
|
||||
var is_empty = _script_list.get_selected_items().size() == 0
|
||||
if(is_empty):
|
||||
_nav.next.disabled = true
|
||||
_nav.prev.disabled = true
|
||||
else:
|
||||
var index = get_selected_index()
|
||||
_nav.prev.disabled = index <= 0
|
||||
_nav.next.disabled = index >= _script_list.get_item_count() - 1
|
||||
|
||||
_nav.run.disabled = is_empty
|
||||
_nav.current_script.disabled = is_empty
|
||||
_nav.show_scripts.disabled = is_empty
|
||||
|
||||
|
||||
# ####################
|
||||
# Public
|
||||
# ####################
|
||||
func run_mode(is_running=true):
|
||||
_run_mode(is_running)
|
||||
|
||||
func set_scripts(scripts):
|
||||
_script_list.clear()
|
||||
for i in range(scripts.size()):
|
||||
_script_list.add_item(scripts[i])
|
||||
_select_script(0)
|
||||
_update_controls()
|
||||
|
||||
func select_script(index):
|
||||
_select_script(index)
|
||||
|
||||
func get_selected_index():
|
||||
return _script_list.get_selected_items()[0]
|
||||
|
||||
func get_log_level():
|
||||
return $LogLevelSlider.value
|
||||
|
||||
func set_log_level(value):
|
||||
$LogLevelSlider.value = _utils.nvl(value, 0)
|
||||
|
||||
func set_ignore_pause(should):
|
||||
_ignore_pauses.pressed = should
|
||||
|
||||
func get_ignore_pause():
|
||||
return _ignore_pauses.pressed
|
||||
|
||||
func get_text_box():
|
||||
return $TextDisplay/RichTextLabel
|
||||
|
||||
func end_run():
|
||||
_run_mode(false)
|
||||
_update_controls()
|
||||
|
||||
func set_progress_script_max(value):
|
||||
_progress.script.set_max(max(value, 1))
|
||||
|
||||
func set_progress_script_value(value):
|
||||
_progress.script.set_value(value)
|
||||
|
||||
func set_progress_test_max(value):
|
||||
_progress.test.set_max(max(value, 1))
|
||||
|
||||
func set_progress_test_value(value):
|
||||
_progress.test.set_value(value)
|
||||
|
||||
func clear_progress():
|
||||
_progress.test.set_value(0)
|
||||
_progress.script.set_value(0)
|
||||
|
||||
func pause():
|
||||
print('we got here')
|
||||
_continue_button.disabled = false
|
||||
|
||||
func set_title(title=null):
|
||||
if(title == null):
|
||||
$TitleBar/Title.set_text(DEFAULT_TITLE)
|
||||
else:
|
||||
$TitleBar/Title.set_text(title)
|
||||
|
||||
func get_run_duration():
|
||||
return $TitleBar/Time.text.to_float()
|
||||
|
||||
func add_passing(amount=1):
|
||||
if(!_summary):
|
||||
return
|
||||
_summary.passing.set_text(str(_summary.passing.get_text().to_int() + amount))
|
||||
$Summary.show()
|
||||
|
||||
func add_failing(amount=1):
|
||||
if(!_summary):
|
||||
return
|
||||
_summary.failing.set_text(str(_summary.failing.get_text().to_int() + amount))
|
||||
$Summary.show()
|
||||
|
||||
func clear_summary():
|
||||
_summary.passing.set_text("0")
|
||||
_summary.failing.set_text("0")
|
||||
$Summary.hide()
|
||||
|
||||
func maximize():
|
||||
if(is_inside_tree()):
|
||||
var vp_size_offset = get_viewport().size
|
||||
rect_size = vp_size_offset / get_scale()
|
||||
set_position(Vector2(0, 0))
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
[gd_scene load_steps=5 format=2]
|
||||
|
||||
[ext_resource path="res://addons/gut/GutScene.gd" type="Script" id=1]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=1]
|
||||
bg_color = Color( 0.193863, 0.205501, 0.214844, 1 )
|
||||
corner_radius_top_left = 20
|
||||
corner_radius_top_right = 20
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=2]
|
||||
bg_color = Color( 1, 1, 1, 1 )
|
||||
border_color = Color( 0, 0, 0, 1 )
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
|
||||
[sub_resource type="Theme" id=3]
|
||||
resource_local_to_scene = true
|
||||
Panel/styles/panel = SubResource( 2 )
|
||||
Panel/styles/panelf = null
|
||||
Panel/styles/panelnc = null
|
||||
|
||||
[node name="Gut" type="Panel"]
|
||||
margin_right = 740.0
|
||||
margin_bottom = 320.0
|
||||
rect_min_size = Vector2( 740, 250 )
|
||||
custom_styles/panel = SubResource( 1 )
|
||||
script = ExtResource( 1 )
|
||||
|
||||
[node name="TitleBar" type="Panel" parent="."]
|
||||
anchor_right = 1.0
|
||||
margin_bottom = 40.0
|
||||
theme = SubResource( 3 )
|
||||
|
||||
[node name="Title" type="Label" parent="TitleBar"]
|
||||
anchor_right = 1.0
|
||||
margin_bottom = 40.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "Gut"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="Time" type="Label" parent="TitleBar"]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
margin_left = -114.0
|
||||
margin_right = -53.0
|
||||
margin_bottom = 40.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "9999.99"
|
||||
valign = 1
|
||||
|
||||
[node name="Maximize" type="Button" parent="TitleBar"]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
margin_left = -30.0
|
||||
margin_top = 10.0
|
||||
margin_right = -6.0
|
||||
margin_bottom = 30.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "M"
|
||||
flat = true
|
||||
|
||||
[node name="ScriptProgress" type="ProgressBar" parent="."]
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 70.0
|
||||
margin_top = -100.0
|
||||
margin_right = 180.0
|
||||
margin_bottom = -70.0
|
||||
step = 1.0
|
||||
|
||||
[node name="Label" type="Label" parent="ScriptProgress"]
|
||||
margin_left = -70.0
|
||||
margin_right = -10.0
|
||||
margin_bottom = 24.0
|
||||
text = "Scripts"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="TestProgress" type="ProgressBar" parent="."]
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 70.0
|
||||
margin_top = -70.0
|
||||
margin_right = 180.0
|
||||
margin_bottom = -40.0
|
||||
step = 1.0
|
||||
|
||||
[node name="Label" type="Label" parent="TestProgress"]
|
||||
margin_left = -70.0
|
||||
margin_right = -10.0
|
||||
margin_bottom = 24.0
|
||||
text = "Tests"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="TextDisplay" type="Panel" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_top = 40.0
|
||||
margin_bottom = -107.0
|
||||
__meta__ = {
|
||||
"_edit_group_": true
|
||||
}
|
||||
|
||||
[node name="RichTextLabel" type="TextEdit" parent="TextDisplay"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_default_cursor_shape = 0
|
||||
readonly = true
|
||||
syntax_highlighting = true
|
||||
smooth_scrolling = true
|
||||
|
||||
[node name="FocusBlocker" type="Panel" parent="TextDisplay"]
|
||||
self_modulate = Color( 1, 1, 1, 0 )
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_right = -10.0
|
||||
|
||||
[node name="Navigation" type="Panel" parent="."]
|
||||
self_modulate = Color( 1, 1, 1, 0 )
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 220.0
|
||||
margin_top = -100.0
|
||||
margin_right = 580.0
|
||||
|
||||
[node name="Previous" type="Button" parent="Navigation"]
|
||||
margin_left = -30.0
|
||||
margin_right = 50.0
|
||||
margin_bottom = 40.0
|
||||
text = "<"
|
||||
|
||||
[node name="Next" type="Button" parent="Navigation"]
|
||||
margin_left = 230.0
|
||||
margin_right = 310.0
|
||||
margin_bottom = 40.0
|
||||
text = ">"
|
||||
|
||||
[node name="Run" type="Button" parent="Navigation"]
|
||||
margin_left = 60.0
|
||||
margin_right = 220.0
|
||||
margin_bottom = 40.0
|
||||
text = "Run"
|
||||
|
||||
[node name="CurrentScript" type="Button" parent="Navigation"]
|
||||
margin_left = -30.0
|
||||
margin_top = 50.0
|
||||
margin_right = 310.0
|
||||
margin_bottom = 90.0
|
||||
text = "res://test/unit/test_gut.gd"
|
||||
clip_text = true
|
||||
|
||||
[node name="ShowScripts" type="Button" parent="Navigation"]
|
||||
margin_left = 320.0
|
||||
margin_top = 50.0
|
||||
margin_right = 360.0
|
||||
margin_bottom = 90.0
|
||||
text = "..."
|
||||
|
||||
[node name="LogLevelSlider" type="HSlider" parent="."]
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 80.0
|
||||
margin_top = -40.0
|
||||
margin_right = 130.0
|
||||
margin_bottom = -20.0
|
||||
rect_scale = Vector2( 2, 2 )
|
||||
max_value = 2.0
|
||||
tick_count = 3
|
||||
ticks_on_borders = true
|
||||
|
||||
[node name="Label" type="Label" parent="LogLevelSlider"]
|
||||
margin_left = -35.0
|
||||
margin_top = 5.0
|
||||
margin_right = 25.0
|
||||
margin_bottom = 25.0
|
||||
rect_scale = Vector2( 0.5, 0.5 )
|
||||
text = "Log Level"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="ScriptsList" type="ItemList" parent="."]
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 180.0
|
||||
margin_top = 40.0
|
||||
margin_right = 620.0
|
||||
margin_bottom = -108.0
|
||||
allow_reselect = true
|
||||
|
||||
[node name="ExtraOptions" type="Panel" parent="."]
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = -210.0
|
||||
margin_top = -246.0
|
||||
margin_bottom = -106.0
|
||||
custom_styles/panel = SubResource( 1 )
|
||||
|
||||
[node name="IgnorePause" type="CheckBox" parent="ExtraOptions"]
|
||||
margin_left = 10.0
|
||||
margin_top = 10.0
|
||||
margin_right = 128.0
|
||||
margin_bottom = 34.0
|
||||
rect_scale = Vector2( 1.5, 1.5 )
|
||||
text = "Ignore Pauses"
|
||||
|
||||
[node name="DisableBlocker" type="CheckBox" parent="ExtraOptions"]
|
||||
margin_left = 10.0
|
||||
margin_top = 50.0
|
||||
margin_right = 130.0
|
||||
margin_bottom = 74.0
|
||||
rect_scale = Vector2( 1.5, 1.5 )
|
||||
text = "Selectable"
|
||||
|
||||
[node name="Copy" type="Button" parent="ExtraOptions"]
|
||||
margin_left = 20.0
|
||||
margin_top = 90.0
|
||||
margin_right = 200.0
|
||||
margin_bottom = 130.0
|
||||
text = "Copy"
|
||||
|
||||
[node name="ResizeHandle" type="Control" parent="."]
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = -40.0
|
||||
margin_top = -40.0
|
||||
|
||||
[node name="Continue" type="Panel" parent="."]
|
||||
self_modulate = Color( 1, 1, 1, 0 )
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = -150.0
|
||||
margin_top = -100.0
|
||||
margin_right = -30.0
|
||||
margin_bottom = -10.0
|
||||
|
||||
[node name="Continue" type="Button" parent="Continue"]
|
||||
margin_top = 50.0
|
||||
margin_right = 119.0
|
||||
margin_bottom = 90.0
|
||||
disabled = true
|
||||
text = "Continue"
|
||||
|
||||
[node name="ShowExtras" type="Button" parent="Continue"]
|
||||
margin_left = 50.0
|
||||
margin_right = 120.0
|
||||
margin_bottom = 40.0
|
||||
rect_pivot_offset = Vector2( 35, 20 )
|
||||
toggle_mode = true
|
||||
text = "_"
|
||||
|
||||
[node name="Summary" type="Node2D" parent="."]
|
||||
position = Vector2( 0, 3 )
|
||||
|
||||
[node name="Passing" type="Label" parent="Summary"]
|
||||
margin_top = 10.0
|
||||
margin_right = 40.0
|
||||
margin_bottom = 24.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "0"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="Failing" type="Label" parent="Summary"]
|
||||
margin_left = 40.0
|
||||
margin_top = 10.0
|
||||
margin_right = 80.0
|
||||
margin_bottom = 24.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "0"
|
||||
align = 1
|
||||
valign = 1
|
||||
[connection signal="mouse_entered" from="TitleBar" to="." method="_on_TitleBar_mouse_entered"]
|
||||
[connection signal="mouse_exited" from="TitleBar" to="." method="_on_TitleBar_mouse_exited"]
|
||||
[connection signal="draw" from="TitleBar/Maximize" to="." method="_on_Maximize_draw"]
|
||||
[connection signal="pressed" from="TitleBar/Maximize" to="." method="_on_Maximize_pressed"]
|
||||
[connection signal="gui_input" from="TextDisplay/RichTextLabel" to="." method="_on_RichTextLabel_gui_input"]
|
||||
[connection signal="gui_input" from="TextDisplay/FocusBlocker" to="." method="_on_FocusBlocker_gui_input"]
|
||||
[connection signal="pressed" from="Navigation/Previous" to="." method="_on_Previous_pressed"]
|
||||
[connection signal="pressed" from="Navigation/Next" to="." method="_on_Next_pressed"]
|
||||
[connection signal="pressed" from="Navigation/Run" to="." method="_on_Run_pressed"]
|
||||
[connection signal="pressed" from="Navigation/CurrentScript" to="." method="_on_CurrentScript_pressed"]
|
||||
[connection signal="pressed" from="Navigation/ShowScripts" to="." method="_on_ShowScripts_pressed"]
|
||||
[connection signal="value_changed" from="LogLevelSlider" to="." method="_on_LogLevelSlider_value_changed"]
|
||||
[connection signal="item_selected" from="ScriptsList" to="." method="_on_ScriptsList_item_selected"]
|
||||
[connection signal="pressed" from="ExtraOptions/IgnorePause" to="." method="_on_IgnorePause_pressed"]
|
||||
[connection signal="toggled" from="ExtraOptions/DisableBlocker" to="." method="_on_DisableBlocker_toggled"]
|
||||
[connection signal="pressed" from="ExtraOptions/Copy" to="." method="_on_Copy_pressed"]
|
||||
[connection signal="mouse_entered" from="ResizeHandle" to="." method="_on_ResizeHandle_mouse_entered"]
|
||||
[connection signal="mouse_exited" from="ResizeHandle" to="." method="_on_ResizeHandle_mouse_exited"]
|
||||
[connection signal="pressed" from="Continue/Continue" to="." method="_on_Continue_pressed"]
|
||||
[connection signal="draw" from="Continue/ShowExtras" to="." method="_on_ShowExtras_draw"]
|
||||
[connection signal="toggled" from="Continue/ShowExtras" to="." method="_on_ShowExtras_toggled"]
|
|
@ -1,22 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
Copyright (c) 2018 Tom "Butch" Wesley
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -1,6 +0,0 @@
|
|||
{func_decleration}
|
||||
__gut_spy('{method_name}', {param_array})
|
||||
if(__gut_should_call_super('{method_name}', {param_array})):
|
||||
return {super_call}
|
||||
else:
|
||||
return __gut_get_stubbed_return('{method_name}', {param_array})
|
|
@ -1,36 +0,0 @@
|
|||
{extends}
|
||||
|
||||
var __gut_metadata_ = {
|
||||
path = '{path}',
|
||||
subpath = '{subpath}',
|
||||
stubber = __gut_instance_from_id({stubber_id}),
|
||||
spy = __gut_instance_from_id({spy_id}),
|
||||
}
|
||||
|
||||
func __gut_instance_from_id(inst_id):
|
||||
if(inst_id == -1):
|
||||
return null
|
||||
else:
|
||||
return instance_from_id(inst_id)
|
||||
|
||||
func __gut_should_call_super(method_name, called_with):
|
||||
if(__gut_metadata_.stubber != null):
|
||||
return __gut_metadata_.stubber.should_call_super(self, method_name, called_with)
|
||||
else:
|
||||
return false
|
||||
|
||||
var __gut_utils_ = load('res://addons/gut/utils.gd').new()
|
||||
|
||||
func __gut_spy(method_name, called_with):
|
||||
if(__gut_metadata_.spy != null):
|
||||
__gut_metadata_.spy.add_call(self, method_name, called_with)
|
||||
|
||||
func __gut_get_stubbed_return(method_name, called_with):
|
||||
if(__gut_metadata_.stubber != null):
|
||||
return __gut_metadata_.stubber.get_return(self, method_name, called_with)
|
||||
else:
|
||||
return null
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Methods start here
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,525 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# Utility class to hold the local and built in methods separately. Add all local
|
||||
# methods FIRST, then add built ins.
|
||||
# ------------------------------------------------------------------------------
|
||||
class ScriptMethods:
|
||||
# List of methods that should not be overloaded when they are not defined
|
||||
# in the class being doubled. These either break things if they are
|
||||
# overloaded or do not have a "super" equivalent so we can't just pass
|
||||
# through.
|
||||
var _blacklist = [
|
||||
'has_method',
|
||||
'get_script',
|
||||
'get',
|
||||
'_notification',
|
||||
'get_path',
|
||||
'_enter_tree',
|
||||
'_exit_tree',
|
||||
'_process',
|
||||
'_draw',
|
||||
'_physics_process',
|
||||
'_input',
|
||||
'_unhandled_input',
|
||||
'_unhandled_key_input',
|
||||
'_set',
|
||||
'_get', # probably
|
||||
'emit_signal', # can't handle extra parameters to be sent with signal.
|
||||
'draw_mesh', # issue with one parameter, value is `Null((..), (..), (..))``
|
||||
'_to_string', # nonexistant function ._to_string
|
||||
'_get_minimum_size', # Nonexistent function _get_minimum_size
|
||||
]
|
||||
|
||||
var built_ins = []
|
||||
var local_methods = []
|
||||
var _method_names = []
|
||||
|
||||
func is_blacklisted(method_meta):
|
||||
return _blacklist.find(method_meta.name) != -1
|
||||
|
||||
func _add_name_if_does_not_have(method_name):
|
||||
var should_add = _method_names.find(method_name) == -1
|
||||
if(should_add):
|
||||
_method_names.append(method_name)
|
||||
return should_add
|
||||
|
||||
func add_built_in_method(method_meta):
|
||||
var did_add = _add_name_if_does_not_have(method_meta.name)
|
||||
if(did_add and !is_blacklisted(method_meta)):
|
||||
built_ins.append(method_meta)
|
||||
|
||||
func add_local_method(method_meta):
|
||||
var did_add = _add_name_if_does_not_have(method_meta.name)
|
||||
if(did_add):
|
||||
local_methods.append(method_meta)
|
||||
|
||||
func to_s():
|
||||
var text = "Locals\n"
|
||||
for i in range(local_methods.size()):
|
||||
text += str(" ", local_methods[i].name, "\n")
|
||||
text += "Built-Ins\n"
|
||||
for i in range(built_ins.size()):
|
||||
text += str(" ", built_ins[i].name, "\n")
|
||||
return text
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Helper class to deal with objects and inner classes.
|
||||
# ------------------------------------------------------------------------------
|
||||
class ObjectInfo:
|
||||
var _path = null
|
||||
var _subpaths = []
|
||||
var _utils = load('res://addons/gut/utils.gd').new()
|
||||
var _method_strategy = null
|
||||
var make_partial_double = false
|
||||
var scene_path = null
|
||||
var _native_class = null
|
||||
var _native_class_instance = null
|
||||
|
||||
func _init(path, subpath=null):
|
||||
_path = path
|
||||
if(subpath != null):
|
||||
_subpaths = _utils.split_string(subpath, '/')
|
||||
|
||||
# Returns an instance of the class/inner class
|
||||
func instantiate():
|
||||
var to_return = null
|
||||
if(is_native()):
|
||||
to_return = _native_class.new()
|
||||
else:
|
||||
to_return = get_loaded_class().new()
|
||||
return to_return
|
||||
|
||||
# Can't call it get_class because that is reserved so it gets this ugly name.
|
||||
# Loads up the class and then any inner classes to give back a reference to
|
||||
# the desired Inner class (if there is any)
|
||||
func get_loaded_class():
|
||||
var LoadedClass = load(_path)
|
||||
for i in range(_subpaths.size()):
|
||||
LoadedClass = LoadedClass.get(_subpaths[i])
|
||||
return LoadedClass
|
||||
|
||||
func to_s():
|
||||
return str(_path, '[', get_subpath(), ']')
|
||||
|
||||
func get_path():
|
||||
return _path
|
||||
|
||||
func get_subpath():
|
||||
return _utils.join_array(_subpaths, '/')
|
||||
|
||||
func has_subpath():
|
||||
return _subpaths.size() != 0
|
||||
|
||||
func get_extends_text():
|
||||
var extend = null
|
||||
if(is_native()):
|
||||
extend = str("extends ", get_native_class_name())
|
||||
else:
|
||||
extend = str("extends '", get_path(), "'")
|
||||
|
||||
if(has_subpath()):
|
||||
extend += str('.', get_subpath().replace('/', '.'))
|
||||
|
||||
return extend
|
||||
|
||||
func get_method_strategy():
|
||||
return _method_strategy
|
||||
|
||||
func set_method_strategy(method_strategy):
|
||||
_method_strategy = method_strategy
|
||||
|
||||
func is_native():
|
||||
return _native_class != null
|
||||
|
||||
func set_native_class(native_class):
|
||||
_native_class = native_class
|
||||
_native_class_instance = native_class.new()
|
||||
_path = _native_class_instance.get_class()
|
||||
|
||||
func get_native_class_name():
|
||||
return _native_class_instance.get_class()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Allows for interacting with a file but only creating a string. This was done
|
||||
# to ease the transition from files being created for doubles to loading
|
||||
# doubles from a string. This allows the files to be created for debugging
|
||||
# purposes since reading a file is easier than reading a dumped out string.
|
||||
# ------------------------------------------------------------------------------
|
||||
class FileOrString:
|
||||
extends File
|
||||
|
||||
var _do_file = false
|
||||
var _contents = ''
|
||||
var _path = null
|
||||
|
||||
func open(path, mode):
|
||||
_path = path
|
||||
if(_do_file):
|
||||
return .open(path, mode)
|
||||
else:
|
||||
return OK
|
||||
|
||||
func close():
|
||||
if(_do_file):
|
||||
return .close()
|
||||
|
||||
func store_string(s):
|
||||
if(_do_file):
|
||||
.store_string(s)
|
||||
_contents += s
|
||||
|
||||
func get_contents():
|
||||
return _contents
|
||||
|
||||
func get_path():
|
||||
return _path
|
||||
|
||||
func load_it():
|
||||
if(_contents != ''):
|
||||
var script = GDScript.new()
|
||||
script.set_source_code(get_contents())
|
||||
script.reload()
|
||||
return script
|
||||
else:
|
||||
return load(_path)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# A stroke of genius if I do say so. This allows for doubling a scene without
|
||||
# having to write any files. By overloading instance we can make whatever
|
||||
# we want.
|
||||
# ------------------------------------------------------------------------------
|
||||
class PackedSceneDouble:
|
||||
extends PackedScene
|
||||
var _script = null
|
||||
var _scene = null
|
||||
|
||||
func set_script_obj(obj):
|
||||
_script = obj
|
||||
|
||||
func instance(edit_state=0):
|
||||
var inst = _scene.instance(edit_state)
|
||||
if(_script != null):
|
||||
inst.set_script(_script)
|
||||
return inst
|
||||
|
||||
func load_scene(path):
|
||||
_scene = load(path)
|
||||
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# START Doubler
|
||||
# ------------------------------------------------------------------------------
|
||||
var _utils = load('res://addons/gut/utils.gd').new()
|
||||
|
||||
var _ignored_methods = _utils.OneToMany.new()
|
||||
var _stubber = _utils.Stubber.new()
|
||||
var _lgr = _utils.get_logger()
|
||||
var _method_maker = _utils.MethodMaker.new()
|
||||
|
||||
var _output_dir = 'user://gut_temp_directory'
|
||||
var _double_count = 0 # used in making files names unique
|
||||
var _spy = null
|
||||
var _strategy = null
|
||||
var _base_script_text = _utils.get_file_as_text('res://addons/gut/double_templates/script_template.gd')
|
||||
var _make_files = false
|
||||
|
||||
# These methods all call super implicitly. Stubbing them to call super causes
|
||||
# super to be called twice.
|
||||
var _non_super_methods = [
|
||||
"_init",
|
||||
"_ready",
|
||||
"_notification",
|
||||
"_enter_world",
|
||||
"_exit_world",
|
||||
"_process",
|
||||
"_physics_process",
|
||||
"_exit_tree",
|
||||
"_gui_input ",
|
||||
]
|
||||
|
||||
func _init(strategy=_utils.DOUBLE_STRATEGY.PARTIAL):
|
||||
set_logger(_utils.get_logger())
|
||||
_strategy = strategy
|
||||
|
||||
# ###############
|
||||
# Private
|
||||
# ###############
|
||||
func _get_indented_line(indents, text):
|
||||
var to_return = ''
|
||||
for _i in range(indents):
|
||||
to_return += "\t"
|
||||
return str(to_return, text, "\n")
|
||||
|
||||
|
||||
func _stub_to_call_super(obj_info, method_name):
|
||||
if(_non_super_methods.has(method_name)):
|
||||
return
|
||||
var path = obj_info.get_path()
|
||||
if(obj_info.scene_path != null):
|
||||
path = obj_info.scene_path
|
||||
var params = _utils.StubParams.new(path, method_name, obj_info.get_subpath())
|
||||
params.to_call_super()
|
||||
_stubber.add_stub(params)
|
||||
|
||||
func _get_base_script_text(obj_info, override_path):
|
||||
var path = obj_info.get_path()
|
||||
if(override_path != null):
|
||||
path = override_path
|
||||
|
||||
var stubber_id = -1
|
||||
if(_stubber != null):
|
||||
stubber_id = _stubber.get_instance_id()
|
||||
|
||||
var spy_id = -1
|
||||
if(_spy != null):
|
||||
spy_id = _spy.get_instance_id()
|
||||
|
||||
var values = {
|
||||
"path":path,
|
||||
"subpath":obj_info.get_subpath(),
|
||||
"stubber_id":stubber_id,
|
||||
"spy_id":spy_id,
|
||||
"extends":obj_info.get_extends_text()
|
||||
}
|
||||
return _base_script_text.format(values)
|
||||
|
||||
func _write_file(obj_info, dest_path, override_path=null):
|
||||
var base_script = _get_base_script_text(obj_info, override_path)
|
||||
var script_methods = _get_methods(obj_info)
|
||||
|
||||
var f = FileOrString.new()
|
||||
f._do_file = _make_files
|
||||
var f_result = f.open(dest_path, f.WRITE)
|
||||
|
||||
if(f_result != OK):
|
||||
_lgr.error(str('Error creating file ', dest_path))
|
||||
_lgr.error(str('Could not create double for :', obj_info.to_s()))
|
||||
return
|
||||
|
||||
f.store_string(base_script)
|
||||
|
||||
for i in range(script_methods.local_methods.size()):
|
||||
if(obj_info.make_partial_double):
|
||||
_stub_to_call_super(obj_info, script_methods.local_methods[i].name)
|
||||
f.store_string(_get_func_text(script_methods.local_methods[i]))
|
||||
|
||||
for i in range(script_methods.built_ins.size()):
|
||||
_stub_to_call_super(obj_info, script_methods.built_ins[i].name)
|
||||
f.store_string(_get_func_text(script_methods.built_ins[i]))
|
||||
|
||||
f.close()
|
||||
return f
|
||||
|
||||
func _double_scene_and_script(scene_info):
|
||||
var to_return = PackedSceneDouble.new()
|
||||
to_return.load_scene(scene_info.get_path())
|
||||
|
||||
var inst = load(scene_info.get_path()).instance()
|
||||
var script_path = null
|
||||
if(inst.get_script()):
|
||||
script_path = inst.get_script().get_path()
|
||||
inst.free()
|
||||
|
||||
if(script_path):
|
||||
var oi = ObjectInfo.new(script_path)
|
||||
oi.set_method_strategy(scene_info.get_method_strategy())
|
||||
oi.make_partial_double = scene_info.make_partial_double
|
||||
oi.scene_path = scene_info.get_path()
|
||||
to_return.set_script_obj(_double(oi, scene_info.get_path()).load_it())
|
||||
|
||||
return to_return
|
||||
|
||||
func _get_methods(object_info):
|
||||
var obj = object_info.instantiate()
|
||||
# any method in the script or super script
|
||||
var script_methods = ScriptMethods.new()
|
||||
var methods = obj.get_method_list()
|
||||
|
||||
# first pass is for local methods only
|
||||
for i in range(methods.size()):
|
||||
# 65 is a magic number for methods in script, though documentation
|
||||
# says 64. This picks up local overloads of base class methods too.
|
||||
if(methods[i].flags == 65 and !_ignored_methods.has(object_info.get_path(), methods[i]['name'])):
|
||||
script_methods.add_local_method(methods[i])
|
||||
|
||||
|
||||
if(object_info.get_method_strategy() == _utils.DOUBLE_STRATEGY.FULL):
|
||||
# second pass is for anything not local
|
||||
for i in range(methods.size()):
|
||||
# 65 is a magic number for methods in script, though documentation
|
||||
# says 64. This picks up local overloads of base class methods too.
|
||||
if(methods[i].flags != 65 and !_ignored_methods.has(object_info.get_path(), methods[i]['name'])):
|
||||
script_methods.add_built_in_method(methods[i])
|
||||
|
||||
return script_methods
|
||||
|
||||
func _get_inst_id_ref_str(inst):
|
||||
var ref_str = 'null'
|
||||
if(inst):
|
||||
ref_str = str('instance_from_id(', inst.get_instance_id(),')')
|
||||
return ref_str
|
||||
|
||||
func _get_func_text(method_hash):
|
||||
return _method_maker.get_function_text(method_hash) + "\n"
|
||||
|
||||
# returns the path to write the double file to
|
||||
func _get_temp_path(object_info):
|
||||
var file_name = null
|
||||
var extension = null
|
||||
if(object_info.is_native()):
|
||||
file_name = object_info.get_native_class_name()
|
||||
extension = 'gd'
|
||||
else:
|
||||
file_name = object_info.get_path().get_file().get_basename()
|
||||
extension = object_info.get_path().get_extension()
|
||||
|
||||
if(object_info.has_subpath()):
|
||||
file_name += '__' + object_info.get_subpath().replace('/', '__')
|
||||
|
||||
file_name += str('__dbl', _double_count, '__.', extension)
|
||||
|
||||
var to_return = _output_dir.plus_file(file_name)
|
||||
return to_return
|
||||
|
||||
func _load_double(fileOrString):
|
||||
return fileOrString.load_it()
|
||||
|
||||
func _double(obj_info, override_path=null):
|
||||
var temp_path = _get_temp_path(obj_info)
|
||||
var result = _write_file(obj_info, temp_path, override_path)
|
||||
_double_count += 1
|
||||
return result
|
||||
|
||||
func _double_script(path, make_partial, strategy):
|
||||
var oi = ObjectInfo.new(path)
|
||||
oi.make_partial_double = make_partial
|
||||
oi.set_method_strategy(strategy)
|
||||
return _double(oi).load_it()
|
||||
|
||||
func _double_inner(path, subpath, make_partial, strategy):
|
||||
var oi = ObjectInfo.new(path, subpath)
|
||||
oi.set_method_strategy(strategy)
|
||||
oi.make_partial_double = make_partial
|
||||
return _double(oi).load_it()
|
||||
|
||||
func _double_scene(path, make_partial, strategy):
|
||||
var oi = ObjectInfo.new(path)
|
||||
oi.set_method_strategy(strategy)
|
||||
oi.make_partial_double = make_partial
|
||||
return _double_scene_and_script(oi)
|
||||
|
||||
func _double_gdnative(native_class, make_partial, strategy):
|
||||
var oi = ObjectInfo.new(null)
|
||||
oi.set_native_class(native_class)
|
||||
oi.set_method_strategy(strategy)
|
||||
oi.make_partial_double = make_partial
|
||||
return _double(oi).load_it()
|
||||
|
||||
# ###############
|
||||
# Public
|
||||
# ###############
|
||||
func get_output_dir():
|
||||
return _output_dir
|
||||
|
||||
func set_output_dir(output_dir):
|
||||
if(output_dir != null):
|
||||
_output_dir = output_dir
|
||||
if(_make_files):
|
||||
var d = Directory.new()
|
||||
d.make_dir_recursive(output_dir)
|
||||
|
||||
func get_spy():
|
||||
return _spy
|
||||
|
||||
func set_spy(spy):
|
||||
_spy = spy
|
||||
|
||||
func get_stubber():
|
||||
return _stubber
|
||||
|
||||
func set_stubber(stubber):
|
||||
_stubber = stubber
|
||||
|
||||
func get_logger():
|
||||
return _lgr
|
||||
|
||||
func set_logger(logger):
|
||||
_lgr = logger
|
||||
_method_maker.set_logger(logger)
|
||||
|
||||
func get_strategy():
|
||||
return _strategy
|
||||
|
||||
func set_strategy(strategy):
|
||||
_strategy = strategy
|
||||
|
||||
func partial_double_scene(path, strategy=_strategy):
|
||||
return _double_scene(path, true, strategy)
|
||||
|
||||
# double a scene
|
||||
func double_scene(path, strategy=_strategy):
|
||||
return _double_scene(path, false, strategy)
|
||||
|
||||
# double a script/object
|
||||
func double(path, strategy=_strategy):
|
||||
return _double_script(path, false, strategy)
|
||||
|
||||
func partial_double(path, strategy=_strategy):
|
||||
return _double_script(path, true, strategy)
|
||||
|
||||
func partial_double_inner(path, subpath, strategy=_strategy):
|
||||
return _double_inner(path, subpath, true, strategy)
|
||||
|
||||
# double an inner class in a script
|
||||
func double_inner(path, subpath, strategy=_strategy):
|
||||
return _double_inner(path, subpath, false, strategy)
|
||||
|
||||
# must always use FULL strategy since this is a native class and you won't get
|
||||
# any methods if you don't use FULL
|
||||
func double_gdnative(native_class):
|
||||
return _double_gdnative(native_class, false, _utils.DOUBLE_STRATEGY.FULL)
|
||||
|
||||
# must always use FULL strategy since this is a native class and you won't get
|
||||
# any methods if you don't use FULL
|
||||
func partial_double_gdnative(native_class):
|
||||
return _double_gdnative(native_class, true, _utils.DOUBLE_STRATEGY.FULL)
|
||||
|
||||
func clear_output_directory():
|
||||
if(!_make_files):
|
||||
return false
|
||||
|
||||
var did = false
|
||||
if(_output_dir.find('user://') == 0):
|
||||
var d = Directory.new()
|
||||
var result = d.open(_output_dir)
|
||||
# BIG GOTCHA HERE. If it cannot open the dir w/ erro 31, then the
|
||||
# directory becomes res:// and things go on normally and gut clears out
|
||||
# out res:// which is SUPER BAD.
|
||||
if(result == OK):
|
||||
d.list_dir_begin(true)
|
||||
var f = d.get_next()
|
||||
while(f != ''):
|
||||
d.remove(f)
|
||||
f = d.get_next()
|
||||
did = true
|
||||
return did
|
||||
|
||||
func delete_output_directory():
|
||||
var did = clear_output_directory()
|
||||
if(did):
|
||||
var d = Directory.new()
|
||||
d.remove(_output_dir)
|
||||
|
||||
func add_ignored_method(path, method_name):
|
||||
_ignored_methods.add(path, method_name)
|
||||
|
||||
func get_ignored_methods():
|
||||
return _ignored_methods
|
||||
|
||||
func get_make_files():
|
||||
return _make_files
|
||||
|
||||
func set_make_files(make_files):
|
||||
_make_files = make_files
|
||||
set_output_dir(_output_dir)
|
1343
addons/gut/gut.gd
1343
addons/gut/gut.gd
File diff suppressed because it is too large
Load diff
|
@ -1,366 +0,0 @@
|
|||
################################################################################
|
||||
#(G)odot (U)nit (T)est class
|
||||
#
|
||||
################################################################################
|
||||
#The MIT License (MIT)
|
||||
#=====================
|
||||
#
|
||||
#Copyright (c) 2019 Tom "Butch" Wesley
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
#of this software and associated documentation files (the "Software"), to deal
|
||||
#in the Software without restriction, including without limitation the rights
|
||||
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
#copies of the Software, and to permit persons to whom the Software is
|
||||
#furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in
|
||||
#all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
#THE SOFTWARE.
|
||||
#
|
||||
################################################################################
|
||||
# Description
|
||||
# -----------
|
||||
# Command line interface for the GUT unit testing tool. Allows you to run tests
|
||||
# from the command line instead of running a scene. Place this script along with
|
||||
# gut.gd into your scripts directory at the root of your project. Once there you
|
||||
# can run this script (from the root of your project) using the following command:
|
||||
# godot -s -d test/gut/gut_cmdln.gd
|
||||
#
|
||||
# See the readme for a list of options and examples. You can also use the -gh
|
||||
# option to get more information about how to use the command line interface.
|
||||
#
|
||||
# Version 6.8.2
|
||||
################################################################################
|
||||
extends SceneTree
|
||||
|
||||
|
||||
var Optparse = load('res://addons/gut/optparse.gd')
|
||||
var Gut = load('res://addons/gut/gut.gd')
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Helper class to resolve the various different places where an option can
|
||||
# be set. Using the get_value method will enforce the order of precedence of:
|
||||
# 1. command line value
|
||||
# 2. config file value
|
||||
# 3. default value
|
||||
#
|
||||
# The idea is that you set the base_opts. That will get you a copies of the
|
||||
# hash with null values for the other types of values. Lower precedented hashes
|
||||
# will punch through null values of higher precedented hashes.
|
||||
#-------------------------------------------------------------------------------
|
||||
class OptionResolver:
|
||||
var base_opts = null
|
||||
var cmd_opts = null
|
||||
var config_opts = null
|
||||
|
||||
|
||||
func get_value(key):
|
||||
return _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key]))
|
||||
|
||||
func set_base_opts(opts):
|
||||
base_opts = opts
|
||||
cmd_opts = _null_copy(opts)
|
||||
config_opts = _null_copy(opts)
|
||||
|
||||
# creates a copy of a hash with all values null.
|
||||
func _null_copy(h):
|
||||
var new_hash = {}
|
||||
for key in h:
|
||||
new_hash[key] = null
|
||||
return new_hash
|
||||
|
||||
func _nvl(a, b):
|
||||
if(a == null):
|
||||
return b
|
||||
else:
|
||||
return a
|
||||
func _string_it(h):
|
||||
var to_return = ''
|
||||
for key in h:
|
||||
to_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')')
|
||||
return to_return
|
||||
|
||||
func to_s():
|
||||
return str("base:\n", _string_it(base_opts), "\n", \
|
||||
"config:\n", _string_it(config_opts), "\n", \
|
||||
"cmd:\n", _string_it(cmd_opts), "\n", \
|
||||
"resolved:\n", _string_it(get_resolved_values()))
|
||||
|
||||
func get_resolved_values():
|
||||
var to_return = {}
|
||||
for key in base_opts:
|
||||
to_return[key] = get_value(key)
|
||||
return to_return
|
||||
|
||||
func to_s_verbose():
|
||||
var to_return = ''
|
||||
var resolved = get_resolved_values()
|
||||
for key in base_opts:
|
||||
to_return += str(key, "\n")
|
||||
to_return += str(' default: ', _nvl(base_opts[key], 'NULL'), "\n")
|
||||
to_return += str(' config: ', _nvl(config_opts[key], ' --'), "\n")
|
||||
to_return += str(' cmd: ', _nvl(cmd_opts[key], ' --'), "\n")
|
||||
to_return += str(' final: ', _nvl(resolved[key], 'NULL'), "\n")
|
||||
|
||||
return to_return
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Here starts the actual script that uses the Options class to kick off Gut
|
||||
# and run your tests.
|
||||
#-------------------------------------------------------------------------------
|
||||
var _utils = load('res://addons/gut/utils.gd').new()
|
||||
# instance of gut
|
||||
var _tester = null
|
||||
# array of command line options specified
|
||||
var _final_opts = []
|
||||
# Hash for easier access to the options in the code. Options will be
|
||||
# extracted into this hash and then the hash will be used afterwards so
|
||||
# that I don't make any dumb typos and get the neat code-sense when I
|
||||
# type a dot.
|
||||
var options = {
|
||||
config_file = 'res://.gutconfig.json',
|
||||
dirs = [],
|
||||
disable_colors = false,
|
||||
double_strategy = 'partial',
|
||||
ignore_pause = false,
|
||||
include_subdirs = false,
|
||||
inner_class = '',
|
||||
log_level = 1,
|
||||
opacity = 100,
|
||||
post_run_script = '',
|
||||
pre_run_script = '',
|
||||
prefix = 'test_',
|
||||
selected = '',
|
||||
should_exit = false,
|
||||
should_exit_on_success = false,
|
||||
should_maximize = false,
|
||||
show_help = false,
|
||||
suffix = '.gd',
|
||||
tests = [],
|
||||
unit_test_name = '',
|
||||
}
|
||||
|
||||
# flag to indicate if only a single script should be run.
|
||||
var _run_single = false
|
||||
|
||||
func setup_options():
|
||||
var opts = Optparse.new()
|
||||
opts.set_banner(('This is the command line interface for the unit testing tool Gut. With this ' +
|
||||
'interface you can run one or more test scripts from the command line. In order ' +
|
||||
'for the Gut options to not clash with any other godot options, each option starts ' +
|
||||
'with a "g". Also, any option that requires a value will take the form of ' +
|
||||
'"-g<name>=<value>". There cannot be any spaces between the option, the "=", or ' +
|
||||
'inside a specified value or godot will think you are trying to run a scene.'))
|
||||
opts.add('-gtest', [], 'Comma delimited list of full paths to test scripts to run.')
|
||||
opts.add('-gdir', [], 'Comma delimited list of directories to add tests from.')
|
||||
opts.add('-gprefix', 'test_', 'Prefix used to find tests when specifying -gdir. Default "[default]"')
|
||||
opts.add('-gsuffix', '.gd', 'Suffix used to find tests when specifying -gdir. Default "[default]"')
|
||||
opts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.')
|
||||
opts.add('-gexit', false, 'Exit after running tests. If not specified you have to manually close the window.')
|
||||
opts.add('-gexit_on_success', false, 'Only exit if all tests pass.')
|
||||
opts.add('-glog', 1, 'Log level. Default [default]')
|
||||
opts.add('-gignore_pause', false, 'Ignores any calls to gut.pause_before_teardown.')
|
||||
opts.add('-gselect', '', ('Select a script to run initially. The first script that ' +
|
||||
'was loaded using -gtest or -gdir that contains the specified ' +
|
||||
'string will be executed. You may run others by interacting ' +
|
||||
'with the GUI.'))
|
||||
opts.add('-gunit_test_name', '', ('Name of a test to run. Any test that contains the specified ' +
|
||||
'text will be run, all others will be skipped.'))
|
||||
opts.add('-gh', false, 'Print this help, then quit')
|
||||
opts.add('-gconfig', 'res://.gutconfig.json', 'A config file that contains configuration information. Default is res://.gutconfig.json')
|
||||
opts.add('-ginner_class', '', 'Only run inner classes that contain this string')
|
||||
opts.add('-gopacity', 100, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.')
|
||||
opts.add('-gpo', false, 'Print option values from all sources and the value used, then quit.')
|
||||
opts.add('-ginclude_subdirs', false, 'Include subdirectories of -gdir.')
|
||||
opts.add('-gdouble_strategy', 'partial', 'Default strategy to use when doubling. Valid values are [partial, full]. Default "[default]"')
|
||||
opts.add('-gdisable_colors', false, 'Disable command line colors.')
|
||||
opts.add('-gpre_run_script', '', 'pre-run hook script path')
|
||||
opts.add('-gpost_run_script', '', 'post-run hook script path')
|
||||
opts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file then quit.')
|
||||
return opts
|
||||
|
||||
|
||||
# Parses options, applying them to the _tester or setting values
|
||||
# in the options struct.
|
||||
func extract_command_line_options(from, to):
|
||||
to.config_file = from.get_value('-gconfig')
|
||||
to.dirs = from.get_value('-gdir')
|
||||
to.disable_colors = from.get_value('-gdisable_colors')
|
||||
to.double_strategy = from.get_value('-gdouble_strategy')
|
||||
to.ignore_pause = from.get_value('-gignore_pause')
|
||||
to.include_subdirs = from.get_value('-ginclude_subdirs')
|
||||
to.inner_class = from.get_value('-ginner_class')
|
||||
to.log_level = from.get_value('-glog')
|
||||
to.opacity = from.get_value('-gopacity')
|
||||
to.post_run_script = from.get_value('-gpost_run_script')
|
||||
to.pre_run_script = from.get_value('-gpre_run_script')
|
||||
to.prefix = from.get_value('-gprefix')
|
||||
to.selected = from.get_value('-gselect')
|
||||
to.should_exit = from.get_value('-gexit')
|
||||
to.should_exit_on_success = from.get_value('-gexit_on_success')
|
||||
to.should_maximize = from.get_value('-gmaximize')
|
||||
to.suffix = from.get_value('-gsuffix')
|
||||
to.tests = from.get_value('-gtest')
|
||||
to.unit_test_name = from.get_value('-gunit_test_name')
|
||||
|
||||
|
||||
func load_options_from_config_file(file_path, into):
|
||||
# SHORTCIRCUIT
|
||||
var f = File.new()
|
||||
if(!f.file_exists(file_path)):
|
||||
if(file_path != 'res://.gutconfig.json'):
|
||||
print('ERROR: Config File "', file_path, '" does not exist.')
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
|
||||
f.open(file_path, f.READ)
|
||||
var json = f.get_as_text()
|
||||
f.close()
|
||||
|
||||
var results = JSON.parse(json)
|
||||
# SHORTCIRCUIT
|
||||
if(results.error != OK):
|
||||
print("\n\n",'!! ERROR parsing file: ', file_path)
|
||||
print(' at line ', results.error_line, ':')
|
||||
print(' ', results.error_string)
|
||||
return -1
|
||||
|
||||
# Get all the options out of the config file using the option name. The
|
||||
# options hash is now the default source of truth for the name of an option.
|
||||
for key in into:
|
||||
if(results.result.has(key)):
|
||||
into[key] = results.result[key]
|
||||
|
||||
return 1
|
||||
|
||||
# Apply all the options specified to _tester. This is where the rubber meets
|
||||
# the road.
|
||||
func apply_options(opts):
|
||||
_tester = Gut.new()
|
||||
get_root().add_child(_tester)
|
||||
_tester.connect('tests_finished', self, '_on_tests_finished', [opts.should_exit, opts.should_exit_on_success])
|
||||
_tester.set_yield_between_tests(true)
|
||||
_tester.set_modulate(Color(1.0, 1.0, 1.0, min(1.0, float(opts.opacity) / 100)))
|
||||
_tester.show()
|
||||
|
||||
_tester.set_include_subdirectories(opts.include_subdirs)
|
||||
|
||||
if(opts.should_maximize):
|
||||
_tester.maximize()
|
||||
|
||||
if(opts.inner_class != ''):
|
||||
_tester.set_inner_class_name(opts.inner_class)
|
||||
_tester.set_log_level(opts.log_level)
|
||||
_tester.set_ignore_pause_before_teardown(opts.ignore_pause)
|
||||
|
||||
for i in range(opts.dirs.size()):
|
||||
_tester.add_directory(opts.dirs[i], opts.prefix, opts.suffix)
|
||||
|
||||
for i in range(opts.tests.size()):
|
||||
_tester.add_script(opts.tests[i])
|
||||
|
||||
if(opts.selected != ''):
|
||||
_tester.select_script(opts.selected)
|
||||
_run_single = true
|
||||
|
||||
if(opts.double_strategy == 'full'):
|
||||
_tester.set_double_strategy(_utils.DOUBLE_STRATEGY.FULL)
|
||||
elif(opts.double_strategy == 'partial'):
|
||||
_tester.set_double_strategy(_utils.DOUBLE_STRATEGY.PARTIAL)
|
||||
|
||||
_tester.set_unit_test_name(opts.unit_test_name)
|
||||
_tester.set_pre_run_script(opts.pre_run_script)
|
||||
_tester.set_post_run_script(opts.post_run_script)
|
||||
_tester.set_color_output(!opts.disable_colors)
|
||||
|
||||
func _print_gutconfigs(values):
|
||||
var header = """Here is a sample of a full .gutconfig.json file.
|
||||
You do not need to specify all values in your own file. The values supplied in
|
||||
this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample
|
||||
option (the resolved values where default < .gutconfig < command line)."""
|
||||
print("\n", header.replace("\n", ' '), "\n\n")
|
||||
var resolved = values
|
||||
|
||||
# remove some options that don't make sense to be in config
|
||||
resolved.erase("config_file")
|
||||
resolved.erase("show_help")
|
||||
|
||||
print("Here's a config with all the properties set based off of your current command and config.")
|
||||
var text = JSON.print(resolved)
|
||||
print(text.replace(',', ",\n"))
|
||||
|
||||
for key in resolved:
|
||||
resolved[key] = null
|
||||
|
||||
print("\n\nAnd here's an empty config for you fill in what you want.")
|
||||
text = JSON.print(resolved)
|
||||
print(text.replace(',', ",\n"))
|
||||
|
||||
|
||||
# parse options and run Gut
|
||||
func _init():
|
||||
var opt_resolver = OptionResolver.new()
|
||||
opt_resolver.set_base_opts(options)
|
||||
|
||||
print("\n\n", ' --- Gut ---')
|
||||
var o = setup_options()
|
||||
|
||||
var all_options_valid = o.parse()
|
||||
extract_command_line_options(o, opt_resolver.cmd_opts)
|
||||
var load_result = \
|
||||
load_options_from_config_file(opt_resolver.get_value('config_file'), opt_resolver.config_opts)
|
||||
|
||||
if(load_result == -1): # -1 indicates json parse error
|
||||
quit()
|
||||
else:
|
||||
if(!all_options_valid):
|
||||
quit()
|
||||
elif(o.get_value('-gh')):
|
||||
var v_info = Engine.get_version_info()
|
||||
print(str('Godot version: ', v_info.major, '.', v_info.minor, '.', v_info.patch))
|
||||
print(str('GUT version: ', Gut.new().get_version()))
|
||||
|
||||
o.print_help()
|
||||
quit()
|
||||
elif(o.get_value('-gpo')):
|
||||
print('All command line options and where they are specified. ' +
|
||||
'The "final" value shows which value will actually be used ' +
|
||||
'based on order of precedence (default < .gutconfig < cmd line).' + "\n")
|
||||
print(opt_resolver.to_s_verbose())
|
||||
quit()
|
||||
elif(o.get_value('-gprint_gutconfig_sample')):
|
||||
_print_gutconfigs(opt_resolver.get_resolved_values())
|
||||
quit()
|
||||
else:
|
||||
_final_opts = opt_resolver.get_resolved_values();
|
||||
apply_options(_final_opts)
|
||||
_tester.test_scripts(!_run_single)
|
||||
|
||||
# exit if option is set.
|
||||
func _on_tests_finished(should_exit, should_exit_on_success):
|
||||
if(_final_opts.dirs.size() == 0):
|
||||
if(_tester.get_summary().get_totals().scripts == 0):
|
||||
var lgr = _tester.get_logger()
|
||||
lgr.error('No directories configured. Add directories with options or a .gutconfig.json file. Use the -gh option for more information.')
|
||||
|
||||
if(_tester.get_fail_count()):
|
||||
OS.exit_code = 1
|
||||
|
||||
# Overwrite the exit code with the post_script
|
||||
var post_inst = _tester.get_post_run_script_instance()
|
||||
if(post_inst != null and post_inst.get_exit_code() != null):
|
||||
OS.exit_code = post_inst.get_exit_code()
|
||||
|
||||
if(should_exit or (should_exit_on_success and _tester.get_fail_count() == 0)):
|
||||
quit()
|
||||
else:
|
||||
print("Tests finished, exit manually")
|
|
@ -1,12 +0,0 @@
|
|||
tool
|
||||
extends EditorPlugin
|
||||
|
||||
func _enter_tree():
|
||||
# Initialization of the plugin goes here
|
||||
# Add the new type with a name, a parent type, a script and an icon
|
||||
add_custom_type("Gut", "Control", preload("gut.gd"), preload("icon.png"))
|
||||
|
||||
func _exit_tree():
|
||||
# Clean-up of the plugin goes here
|
||||
# Always remember to remove it from the engine when deactivated
|
||||
remove_custom_type("Gut")
|
|
@ -1,35 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This script is the base for custom scripts to be used in pre and post
|
||||
# run hooks.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# This is the instance of GUT that is running the tests. You can get
|
||||
# information about the run from this object. This is set by GUT when the
|
||||
# script is instantiated.
|
||||
var gut = null
|
||||
|
||||
# the exit code to be used by gut_cmdln. See set method.
|
||||
var _exit_code = null
|
||||
|
||||
var _should_abort = false
|
||||
# Virtual method that will be called by GUT after instantiating
|
||||
# this script.
|
||||
func run():
|
||||
pass
|
||||
|
||||
# Set the exit code when running from the command line. If not set then the
|
||||
# default exit code will be returned (0 when no tests fail, 1 when any tests
|
||||
# fail).
|
||||
func set_exit_code(code):
|
||||
_exit_code = code
|
||||
|
||||
func get_exit_code():
|
||||
return _exit_code
|
||||
|
||||
# Usable by pre-run script to cause the run to end AFTER the run() method
|
||||
# finishes. post-run script will not be ran.
|
||||
func abort():
|
||||
_should_abort = true
|
||||
|
||||
func should_abort():
|
||||
return _should_abort
|
Binary file not shown.
Before Width: | Height: | Size: 320 B |
|
@ -1,105 +0,0 @@
|
|||
extends Node2D
|
||||
|
||||
var _gut = null
|
||||
|
||||
var types = {
|
||||
warn = 'WARNING',
|
||||
error = 'ERROR',
|
||||
info = 'INFO',
|
||||
debug = 'DEBUG',
|
||||
deprecated = 'DEPRECATED'
|
||||
}
|
||||
|
||||
var _logs = {
|
||||
types.warn: [],
|
||||
types.error: [],
|
||||
types.info: [],
|
||||
types.debug: [],
|
||||
types.deprecated: []
|
||||
}
|
||||
|
||||
var _suppress_output = false
|
||||
|
||||
func _gut_log_level_for_type(log_type):
|
||||
if(log_type == types.warn or log_type == types.error or log_type == types.deprecated):
|
||||
return 0
|
||||
else:
|
||||
return 2
|
||||
|
||||
func _log(type, text):
|
||||
_logs[type].append(text)
|
||||
var formatted = str('[', type, '] ', text)
|
||||
if(!_suppress_output):
|
||||
if(_gut):
|
||||
# this will keep the text indented under test for readability
|
||||
_gut.p(formatted, _gut_log_level_for_type(type))
|
||||
# IDEA! We could store the current script and test that generated
|
||||
# this output, which could be useful later if we printed out a summary.
|
||||
else:
|
||||
print(formatted)
|
||||
return formatted
|
||||
|
||||
# ---------------
|
||||
# Get Methods
|
||||
# ---------------
|
||||
func get_warnings():
|
||||
return get_log_entries(types.warn)
|
||||
|
||||
func get_errors():
|
||||
return get_log_entries(types.error)
|
||||
|
||||
func get_infos():
|
||||
return get_log_entries(types.info)
|
||||
|
||||
func get_debugs():
|
||||
return get_log_entries(types.debug)
|
||||
|
||||
func get_deprecated():
|
||||
return get_log_entries(types.deprecated)
|
||||
|
||||
func get_count(log_type=null):
|
||||
var count = 0
|
||||
if(log_type == null):
|
||||
for key in _logs:
|
||||
count += _logs[key].size()
|
||||
else:
|
||||
count = _logs[log_type].size()
|
||||
return count
|
||||
|
||||
func get_log_entries(log_type):
|
||||
return _logs[log_type]
|
||||
|
||||
# ---------------
|
||||
# Log methods
|
||||
# ---------------
|
||||
func warn(text):
|
||||
return _log(types.warn, text)
|
||||
|
||||
func error(text):
|
||||
return _log(types.error, text)
|
||||
|
||||
func info(text):
|
||||
return _log(types.info, text)
|
||||
|
||||
func debug(text):
|
||||
return _log(types.debug, text)
|
||||
|
||||
# supply some text or the name of the deprecated method and the replacement.
|
||||
func deprecated(text, alt_method=null):
|
||||
var msg = text
|
||||
if(alt_method):
|
||||
msg = str('The method ', text, ' is deprecated, use ', alt_method , ' instead.')
|
||||
return _log(types.deprecated, msg)
|
||||
|
||||
# ---------------
|
||||
# Misc
|
||||
# ---------------
|
||||
func get_gut():
|
||||
return _gut
|
||||
|
||||
func set_gut(gut):
|
||||
_gut = gut
|
||||
|
||||
func clear():
|
||||
for key in _logs:
|
||||
_logs[key].clear()
|
|
@ -1,211 +0,0 @@
|
|||
# This class will generate method declaration lines based on method meta
|
||||
# data. It will create defaults that match the method data.
|
||||
#
|
||||
# --------------------
|
||||
# function meta data
|
||||
# --------------------
|
||||
# name:
|
||||
# flags:
|
||||
# args: [{
|
||||
# (class_name:),
|
||||
# (hint:0),
|
||||
# (hint_string:),
|
||||
# (name:),
|
||||
# (type:4),
|
||||
# (usage:7)
|
||||
# }]
|
||||
# default_args []
|
||||
|
||||
var _utils = load('res://addons/gut/utils.gd').new()
|
||||
var _lgr = _utils.get_logger()
|
||||
const PARAM_PREFIX = 'p_'
|
||||
|
||||
# ------------------------------------------------------
|
||||
# _supported_defaults
|
||||
#
|
||||
# This array contains all the data types that are supported for default values.
|
||||
# If a value is supported it will contain either an empty string or a prefix
|
||||
# that should be used when setting the parameter default value.
|
||||
# For example int, real, bool do not need anything func(p1=1, p2=2.2, p3=false)
|
||||
# but things like Vectors and Colors do since only the parameters to create a
|
||||
# new Vector or Color are included in the metadata.
|
||||
# ------------------------------------------------------
|
||||
# TYPE_NIL = 0 — Variable is of type nil (only applied for null).
|
||||
# TYPE_BOOL = 1 — Variable is of type bool.
|
||||
# TYPE_INT = 2 — Variable is of type int.
|
||||
# TYPE_REAL = 3 — Variable is of type float/real.
|
||||
# TYPE_STRING = 4 — Variable is of type String.
|
||||
# TYPE_VECTOR2 = 5 — Variable is of type Vector2.
|
||||
# TYPE_RECT2 = 6 — Variable is of type Rect2.
|
||||
# TYPE_VECTOR3 = 7 — Variable is of type Vector3.
|
||||
# TYPE_COLOR = 14 — Variable is of type Color.
|
||||
# TYPE_OBJECT = 17 — Variable is of type Object.
|
||||
# TYPE_DICTIONARY = 18 — Variable is of type Dictionary.
|
||||
# TYPE_ARRAY = 19 — Variable is of type Array.
|
||||
# TYPE_VECTOR2_ARRAY = 24 — Variable is of type PoolVector2Array.
|
||||
|
||||
|
||||
|
||||
# TYPE_TRANSFORM2D = 8 — Variable is of type Transform2D.
|
||||
# TYPE_PLANE = 9 — Variable is of type Plane.
|
||||
# TYPE_QUAT = 10 — Variable is of type Quat.
|
||||
# TYPE_AABB = 11 — Variable is of type AABB.
|
||||
# TYPE_BASIS = 12 — Variable is of type Basis.
|
||||
# TYPE_TRANSFORM = 13 — Variable is of type Transform.
|
||||
# TYPE_NODE_PATH = 15 — Variable is of type NodePath.
|
||||
# TYPE_RID = 16 — Variable is of type RID.
|
||||
# TYPE_RAW_ARRAY = 20 — Variable is of type PoolByteArray.
|
||||
# TYPE_INT_ARRAY = 21 — Variable is of type PoolIntArray.
|
||||
# TYPE_REAL_ARRAY = 22 — Variable is of type PoolRealArray.
|
||||
# TYPE_STRING_ARRAY = 23 — Variable is of type PoolStringArray.
|
||||
# TYPE_VECTOR3_ARRAY = 25 — Variable is of type PoolVector3Array.
|
||||
# TYPE_COLOR_ARRAY = 26 — Variable is of type PoolColorArray.
|
||||
# TYPE_MAX = 27 — Marker for end of type constants.
|
||||
# ------------------------------------------------------
|
||||
var _supported_defaults = []
|
||||
|
||||
func _init():
|
||||
for _i in range(TYPE_MAX):
|
||||
_supported_defaults.append(null)
|
||||
|
||||
# These types do not require a prefix for defaults
|
||||
_supported_defaults[TYPE_NIL] = ''
|
||||
_supported_defaults[TYPE_BOOL] = ''
|
||||
_supported_defaults[TYPE_INT] = ''
|
||||
_supported_defaults[TYPE_REAL] = ''
|
||||
_supported_defaults[TYPE_OBJECT] = ''
|
||||
_supported_defaults[TYPE_ARRAY] = ''
|
||||
_supported_defaults[TYPE_STRING] = ''
|
||||
_supported_defaults[TYPE_DICTIONARY] = ''
|
||||
_supported_defaults[TYPE_VECTOR2_ARRAY] = ''
|
||||
|
||||
# These require a prefix for whatever default is provided
|
||||
_supported_defaults[TYPE_VECTOR2] = 'Vector2'
|
||||
_supported_defaults[TYPE_RECT2] = 'Rect2'
|
||||
_supported_defaults[TYPE_VECTOR3] = 'Vector3'
|
||||
_supported_defaults[TYPE_COLOR] = 'Color'
|
||||
|
||||
# ###############
|
||||
# Private
|
||||
# ###############
|
||||
var _func_text = _utils.get_file_as_text('res://addons/gut/double_templates/function_template.gd')
|
||||
|
||||
func _is_supported_default(type_flag):
|
||||
return type_flag >= 0 and type_flag < _supported_defaults.size() and [type_flag] != null
|
||||
|
||||
# Creates a list of parameters with defaults of null unless a default value is
|
||||
# found in the metadata. If a default is found in the meta then it is used if
|
||||
# it is one we know how support.
|
||||
#
|
||||
# If a default is found that we don't know how to handle then this method will
|
||||
# return null.
|
||||
func _get_arg_text(method_meta):
|
||||
var text = ''
|
||||
var args = method_meta.args
|
||||
var defaults = []
|
||||
var has_unsupported_defaults = false
|
||||
|
||||
# fill up the defaults with null defaults for everything that doesn't have
|
||||
# a default in the meta data. default_args is an array of default values
|
||||
# for the last n parameters where n is the size of default_args so we only
|
||||
# add nulls for everything up to the first parameter with a default.
|
||||
for _i in range(args.size() - method_meta.default_args.size()):
|
||||
defaults.append('null')
|
||||
|
||||
# Add meta-data defaults.
|
||||
for i in range(method_meta.default_args.size()):
|
||||
var t = args[defaults.size()]['type']
|
||||
var value = ''
|
||||
if(_is_supported_default(t)):
|
||||
# strings are special, they need quotes around the value
|
||||
if(t == TYPE_STRING):
|
||||
value = str("'", str(method_meta.default_args[i]), "'")
|
||||
# Colors need the parens but things like Vector2 and Rect2 don't
|
||||
elif(t == TYPE_COLOR):
|
||||
value = str(_supported_defaults[t], '(', str(method_meta.default_args[i]), ')')
|
||||
elif(t == TYPE_OBJECT):
|
||||
if(str(method_meta.default_args[i]) == "[Object:null]"):
|
||||
value = str(_supported_defaults[t], 'null')
|
||||
else:
|
||||
value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower())
|
||||
|
||||
# Everything else puts the prefix (if one is there) form _supported_defaults
|
||||
# in front. The to_lower is used b/c for some reason the defaults for
|
||||
# null, true, false are all "Null", "True", "False".
|
||||
else:
|
||||
value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower())
|
||||
else:
|
||||
_lgr.warn(str(
|
||||
'Unsupported default param type: ',method_meta.name, '-', args[defaults.size()].name, ' ', t, ' = ', method_meta.default_args[i]))
|
||||
value = str('unsupported=',t)
|
||||
has_unsupported_defaults = true
|
||||
|
||||
defaults.append(value)
|
||||
|
||||
# construct the string of parameters
|
||||
for i in range(args.size()):
|
||||
text += str(PARAM_PREFIX, args[i].name, '=', defaults[i])
|
||||
if(i != args.size() -1):
|
||||
text += ', '
|
||||
|
||||
# if we don't know how to make a default then we have to return null b/c
|
||||
# it will cause a runtime error and it's one thing we could return to let
|
||||
# callers know it didn't work.
|
||||
if(has_unsupported_defaults):
|
||||
text = null
|
||||
|
||||
return text
|
||||
|
||||
# ###############
|
||||
# Public
|
||||
# ###############
|
||||
|
||||
# Creates a delceration for a function based off of function metadata. All
|
||||
# types whose defaults are supported will have their values. If a datatype
|
||||
# is not supported and the parameter has a default, a warning message will be
|
||||
# printed and the declaration will return null.
|
||||
func get_function_text(meta):
|
||||
var method_params = _get_arg_text(meta)
|
||||
var text = null
|
||||
|
||||
var param_array = get_spy_call_parameters_text(meta)
|
||||
if(param_array == 'null'):
|
||||
param_array = '[]'
|
||||
|
||||
if(method_params != null):
|
||||
var decleration = str('func ', meta.name, '(', method_params, '):')
|
||||
text = _func_text.format({
|
||||
"func_decleration":decleration,
|
||||
"method_name":meta.name,
|
||||
"param_array":param_array,
|
||||
"super_call":get_super_call_text(meta)
|
||||
})
|
||||
return text
|
||||
|
||||
# creates a call to the function in meta in the super's class.
|
||||
func get_super_call_text(meta):
|
||||
var params = ''
|
||||
|
||||
for i in range(meta.args.size()):
|
||||
params += PARAM_PREFIX + meta.args[i].name
|
||||
if(meta.args.size() > 1 and i != meta.args.size() -1):
|
||||
params += ', '
|
||||
|
||||
return str('.', meta.name, '(', params, ')')
|
||||
|
||||
func get_spy_call_parameters_text(meta):
|
||||
var called_with = 'null'
|
||||
if(meta.args.size() > 0):
|
||||
called_with = '['
|
||||
for i in range(meta.args.size()):
|
||||
called_with += str(PARAM_PREFIX, meta.args[i].name)
|
||||
if(i < meta.args.size() - 1):
|
||||
called_with += ', '
|
||||
called_with += ']'
|
||||
return called_with
|
||||
|
||||
func get_logger():
|
||||
return _lgr
|
||||
|
||||
func set_logger(logger):
|
||||
_lgr = logger
|
|
@ -1,38 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This datastructure represents a simple one-to-many relationship. It manages
|
||||
# a dictionary of value/array pairs. It ignores duplicates of both the "one"
|
||||
# and the "many".
|
||||
# ------------------------------------------------------------------------------
|
||||
var _items = {}
|
||||
|
||||
# return the size of _items or the size of an element in _items if "one" was
|
||||
# specified.
|
||||
func size(one=null):
|
||||
var to_return = 0
|
||||
if(one == null):
|
||||
to_return = _items.size()
|
||||
elif(_items.has(one)):
|
||||
to_return = _items[one].size()
|
||||
return to_return
|
||||
|
||||
# Add an element to "one" if it does not already exist
|
||||
func add(one, many_item):
|
||||
if(_items.has(one) and !_items[one].has(many_item)):
|
||||
_items[one].append(many_item)
|
||||
else:
|
||||
_items[one] = [many_item]
|
||||
|
||||
func clear():
|
||||
_items.clear()
|
||||
|
||||
func has(one, many_item):
|
||||
var to_return = false
|
||||
if(_items.has(one)):
|
||||
to_return = _items[one].has(many_item)
|
||||
return to_return
|
||||
|
||||
func to_s():
|
||||
var to_return = ''
|
||||
for key in _items:
|
||||
to_return += str(key, ": ", _items[key], "\n")
|
||||
return to_return
|
|
@ -1,250 +0,0 @@
|
|||
################################################################################
|
||||
#(G)odot (U)nit (T)est class
|
||||
#
|
||||
################################################################################
|
||||
#The MIT License (MIT)
|
||||
#=====================
|
||||
#
|
||||
#Copyright (c) 2019 Tom "Butch" Wesley
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
#of this software and associated documentation files (the "Software"), to deal
|
||||
#in the Software without restriction, including without limitation the rights
|
||||
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
#copies of the Software, and to permit persons to whom the Software is
|
||||
#furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in
|
||||
#all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
#THE SOFTWARE.
|
||||
#
|
||||
################################################################################
|
||||
# Description
|
||||
# -----------
|
||||
# Command line interface for the GUT unit testing tool. Allows you to run tests
|
||||
# from the command line instead of running a scene. Place this script along with
|
||||
# gut.gd into your scripts directory at the root of your project. Once there you
|
||||
# can run this script (from the root of your project) using the following command:
|
||||
# godot -s -d test/gut/gut_cmdln.gd
|
||||
#
|
||||
# See the readme for a list of options and examples. You can also use the -gh
|
||||
# option to get more information about how to use the command line interface.
|
||||
#
|
||||
# Version 6.8.2
|
||||
################################################################################
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Parses the command line arguments supplied into an array that can then be
|
||||
# examined and parsed based on how the gut options work.
|
||||
#-------------------------------------------------------------------------------
|
||||
class CmdLineParser:
|
||||
var _used_options = []
|
||||
# an array of arrays. Each element in this array will contain an option
|
||||
# name and if that option contains a value then it will have a sedond
|
||||
# element. For example:
|
||||
# [[-gselect, test.gd], [-gexit]]
|
||||
var _opts = []
|
||||
|
||||
func _init():
|
||||
for i in range(OS.get_cmdline_args().size()):
|
||||
var opt_val = OS.get_cmdline_args()[i].split('=')
|
||||
_opts.append(opt_val)
|
||||
|
||||
# Parse out multiple comma delimited values from a command line
|
||||
# option. Values are separated from option name with "=" and
|
||||
# additional values are comma separated.
|
||||
func _parse_array_value(full_option):
|
||||
var value = _parse_option_value(full_option)
|
||||
var split = value.split(',')
|
||||
return split
|
||||
|
||||
# Parse out the value of an option. Values are separated from
|
||||
# the option name with "="
|
||||
func _parse_option_value(full_option):
|
||||
if(full_option.size() > 1):
|
||||
return full_option[1]
|
||||
else:
|
||||
return null
|
||||
|
||||
# Search _opts for an element that starts with the option name
|
||||
# specified.
|
||||
func find_option(name):
|
||||
var found = false
|
||||
var idx = 0
|
||||
|
||||
while(idx < _opts.size() and !found):
|
||||
if(_opts[idx][0] == name):
|
||||
found = true
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
if(found):
|
||||
return idx
|
||||
else:
|
||||
return -1
|
||||
|
||||
func get_array_value(option):
|
||||
_used_options.append(option)
|
||||
var to_return = []
|
||||
var opt_loc = find_option(option)
|
||||
if(opt_loc != -1):
|
||||
to_return = _parse_array_value(_opts[opt_loc])
|
||||
_opts.remove(opt_loc)
|
||||
|
||||
return to_return
|
||||
|
||||
# returns the value of an option if it was specified, null otherwise. This
|
||||
# used to return the default but that became problemnatic when trying to
|
||||
# punch through the different places where values could be specified.
|
||||
func get_value(option):
|
||||
_used_options.append(option)
|
||||
var to_return = null
|
||||
var opt_loc = find_option(option)
|
||||
if(opt_loc != -1):
|
||||
to_return = _parse_option_value(_opts[opt_loc])
|
||||
_opts.remove(opt_loc)
|
||||
|
||||
return to_return
|
||||
|
||||
# returns true if it finds the option, false if not.
|
||||
func was_specified(option):
|
||||
_used_options.append(option)
|
||||
return find_option(option) != -1
|
||||
|
||||
# Returns any unused command line options. I found that only the -s and
|
||||
# script name come through from godot, all other options that godot uses
|
||||
# are not sent through OS.get_cmdline_args().
|
||||
#
|
||||
# This is a onetime thing b/c i kill all items in _used_options
|
||||
func get_unused_options():
|
||||
var to_return = []
|
||||
for i in range(_opts.size()):
|
||||
to_return.append(_opts[i][0])
|
||||
|
||||
var script_option = to_return.find('-s')
|
||||
if script_option != -1:
|
||||
to_return.remove(script_option + 1)
|
||||
to_return.remove(script_option)
|
||||
|
||||
while(_used_options.size() > 0):
|
||||
var index = to_return.find(_used_options[0].split("=")[0])
|
||||
if(index != -1):
|
||||
to_return.remove(index)
|
||||
_used_options.remove(0)
|
||||
|
||||
return to_return
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Simple class to hold a command line option
|
||||
#-------------------------------------------------------------------------------
|
||||
class Option:
|
||||
var value = null
|
||||
var option_name = ''
|
||||
var default = null
|
||||
var description = ''
|
||||
|
||||
func _init(name, default_value, desc=''):
|
||||
option_name = name
|
||||
default = default_value
|
||||
description = desc
|
||||
value = null#default_value
|
||||
|
||||
func pad(to_pad, size, pad_with=' '):
|
||||
var to_return = to_pad
|
||||
for _i in range(to_pad.length(), size):
|
||||
to_return += pad_with
|
||||
|
||||
return to_return
|
||||
|
||||
func to_s(min_space=0):
|
||||
var subbed_desc = description
|
||||
if(subbed_desc.find('[default]') != -1):
|
||||
subbed_desc = subbed_desc.replace('[default]', str(default))
|
||||
return pad(option_name, min_space) + subbed_desc
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# The high level interface between this script and the command line options
|
||||
# supplied. Uses Option class and CmdLineParser to extract information from
|
||||
# the command line and make it easily accessible.
|
||||
#-------------------------------------------------------------------------------
|
||||
var options = []
|
||||
var _opts = []
|
||||
var _banner = ''
|
||||
|
||||
func add(name, default, desc):
|
||||
options.append(Option.new(name, default, desc))
|
||||
|
||||
func get_value(name):
|
||||
var found = false
|
||||
var idx = 0
|
||||
|
||||
while(idx < options.size() and !found):
|
||||
if(options[idx].option_name == name):
|
||||
found = true
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
if(found):
|
||||
return options[idx].value
|
||||
else:
|
||||
print("COULD NOT FIND OPTION " + name)
|
||||
return null
|
||||
|
||||
func set_banner(banner):
|
||||
_banner = banner
|
||||
|
||||
func print_help():
|
||||
var longest = 0
|
||||
for i in range(options.size()):
|
||||
if(options[i].option_name.length() > longest):
|
||||
longest = options[i].option_name.length()
|
||||
|
||||
print('---------------------------------------------------------')
|
||||
print(_banner)
|
||||
|
||||
print("\nOptions\n-------")
|
||||
for i in range(options.size()):
|
||||
print(' ' + options[i].to_s(longest + 2))
|
||||
print('---------------------------------------------------------')
|
||||
|
||||
func print_options():
|
||||
for i in range(options.size()):
|
||||
print(options[i].option_name + '=' + str(options[i].value))
|
||||
|
||||
func parse():
|
||||
var parser = CmdLineParser.new()
|
||||
|
||||
for i in range(options.size()):
|
||||
var t = typeof(options[i].default)
|
||||
# only set values that were specified at the command line so that
|
||||
# we can punch through default and config values correctly later.
|
||||
# Without this check, you can't tell the difference between the
|
||||
# defaults and what was specified, so you can't punch through
|
||||
# higher level options.
|
||||
if(parser.was_specified(options[i].option_name)):
|
||||
if(t == TYPE_INT):
|
||||
options[i].value = int(parser.get_value(options[i].option_name))
|
||||
elif(t == TYPE_STRING):
|
||||
options[i].value = parser.get_value(options[i].option_name)
|
||||
elif(t == TYPE_ARRAY):
|
||||
options[i].value = parser.get_array_value(options[i].option_name)
|
||||
elif(t == TYPE_BOOL):
|
||||
options[i].value = parser.was_specified(options[i].option_name)
|
||||
elif(t == TYPE_NIL):
|
||||
print(options[i].option_name + ' cannot be processed, it has a nil datatype')
|
||||
else:
|
||||
print(options[i].option_name + ' cannot be processed, it has unknown datatype:' + str(t))
|
||||
|
||||
var unused = parser.get_unused_options()
|
||||
if(unused.size() > 0):
|
||||
print("Unrecognized options: ", unused)
|
||||
return false
|
||||
|
||||
return true
|
|
@ -1,7 +0,0 @@
|
|||
[plugin]
|
||||
|
||||
name="Gut"
|
||||
description="Unit Testing tool for Godot."
|
||||
author="Butch Wesley"
|
||||
version="6.8.2"
|
||||
script="gut_plugin.gd"
|
|
@ -1,166 +0,0 @@
|
|||
################################################################################
|
||||
#The MIT License (MIT)
|
||||
#=====================
|
||||
#
|
||||
#Copyright (c) 2019 Tom "Butch" Wesley
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
#of this software and associated documentation files (the "Software"), to deal
|
||||
#in the Software without restriction, including without limitation the rights
|
||||
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
#copies of the Software, and to permit persons to whom the Software is
|
||||
#furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in
|
||||
#all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
#THE SOFTWARE.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
# Some arbitrary string that should never show up by accident. If it does, then
|
||||
# shame on you.
|
||||
const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_'
|
||||
|
||||
# This hash holds the objects that are being watched, the signals that are being
|
||||
# watched, and an array of arrays that contains arguments that were passed
|
||||
# each time the signal was emitted.
|
||||
#
|
||||
# For example:
|
||||
# _watched_signals => {
|
||||
# ref1 => {
|
||||
# 'signal1' => [[], [], []],
|
||||
# 'signal2' => [[p1, p2]],
|
||||
# 'signal3' => [[p1]]
|
||||
# },
|
||||
# ref2 => {
|
||||
# 'some_signal' => [],
|
||||
# 'other_signal' => [[p1, p2, p3], [p1, p2, p3], [p1, p2, p3]]
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# In this sample:
|
||||
# - signal1 on the ref1 object was emitted 3 times and each time, zero
|
||||
# parameters were passed.
|
||||
# - signal3 on ref1 was emitted once and passed a single parameter
|
||||
# - some_signal on ref2 was never emitted.
|
||||
# - other_signal on ref2 was emitted 3 times, each time with 3 parameters.
|
||||
var _watched_signals = {}
|
||||
var _utils = load('res://addons/gut/utils.gd').new()
|
||||
|
||||
func _add_watched_signal(obj, name):
|
||||
# SHORTCIRCUIT - ignore dupes
|
||||
if(_watched_signals.has(obj) and _watched_signals[obj].has(name)):
|
||||
return
|
||||
|
||||
if(!_watched_signals.has(obj)):
|
||||
_watched_signals[obj] = {name:[]}
|
||||
else:
|
||||
_watched_signals[obj][name] = []
|
||||
obj.connect(name, self, '_on_watched_signal', [obj, name])
|
||||
|
||||
# This handles all the signals that are watched. It supports up to 9 parameters
|
||||
# which could be emitted by the signal and the two parameters used when it is
|
||||
# connected via watch_signal. I chose 9 since you can only specify up to 9
|
||||
# parameters when dynamically calling a method via call (per the Godot
|
||||
# documentation, i.e. some_object.call('some_method', 1, 2, 3...)).
|
||||
#
|
||||
# Based on the documentation of emit_signal, it appears you can only pass up
|
||||
# to 4 parameters when firing a signal. I haven't verified this, but this should
|
||||
# future proof this some if the value ever grows.
|
||||
func _on_watched_signal(arg1=ARG_NOT_SET, arg2=ARG_NOT_SET, arg3=ARG_NOT_SET, \
|
||||
arg4=ARG_NOT_SET, arg5=ARG_NOT_SET, arg6=ARG_NOT_SET, \
|
||||
arg7=ARG_NOT_SET, arg8=ARG_NOT_SET, arg9=ARG_NOT_SET, \
|
||||
arg10=ARG_NOT_SET, arg11=ARG_NOT_SET):
|
||||
var args = [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11]
|
||||
|
||||
# strip off any unused vars.
|
||||
var idx = args.size() -1
|
||||
while(str(args[idx]) == ARG_NOT_SET):
|
||||
args.remove(idx)
|
||||
idx -= 1
|
||||
|
||||
# retrieve object and signal name from the array and remove them. These
|
||||
# will always be at the end since they are added when the connect happens.
|
||||
var signal_name = args[args.size() -1]
|
||||
args.pop_back()
|
||||
var object = args[args.size() -1]
|
||||
args.pop_back()
|
||||
|
||||
_watched_signals[object][signal_name].append(args)
|
||||
|
||||
func does_object_have_signal(object, signal_name):
|
||||
var signals = object.get_signal_list()
|
||||
for i in range(signals.size()):
|
||||
if(signals[i]['name'] == signal_name):
|
||||
return true
|
||||
return false
|
||||
|
||||
func watch_signals(object):
|
||||
var signals = object.get_signal_list()
|
||||
for i in range(signals.size()):
|
||||
_add_watched_signal(object, signals[i]['name'])
|
||||
|
||||
func watch_signal(object, signal_name):
|
||||
var did = false
|
||||
if(does_object_have_signal(object, signal_name)):
|
||||
_add_watched_signal(object, signal_name)
|
||||
did = true
|
||||
return did
|
||||
|
||||
func get_emit_count(object, signal_name):
|
||||
var to_return = -1
|
||||
if(is_watching(object, signal_name)):
|
||||
to_return = _watched_signals[object][signal_name].size()
|
||||
return to_return
|
||||
|
||||
func did_emit(object, signal_name):
|
||||
var did = false
|
||||
if(is_watching(object, signal_name)):
|
||||
did = get_emit_count(object, signal_name) != 0
|
||||
return did
|
||||
|
||||
func print_object_signals(object):
|
||||
var list = object.get_signal_list()
|
||||
for i in range(list.size()):
|
||||
print(list[i].name, "\n ", list[i])
|
||||
|
||||
func get_signal_parameters(object, signal_name, index=-1):
|
||||
var params = null
|
||||
if(is_watching(object, signal_name)):
|
||||
var all_params = _watched_signals[object][signal_name]
|
||||
if(all_params.size() > 0):
|
||||
if(index == -1):
|
||||
index = all_params.size() -1
|
||||
params = all_params[index]
|
||||
return params
|
||||
|
||||
func is_watching_object(object):
|
||||
return _watched_signals.has(object)
|
||||
|
||||
func is_watching(object, signal_name):
|
||||
return _watched_signals.has(object) and _watched_signals[object].has(signal_name)
|
||||
|
||||
func clear():
|
||||
for obj in _watched_signals:
|
||||
for signal_name in _watched_signals[obj]:
|
||||
if(_utils.is_not_freed(obj)):
|
||||
obj.disconnect(signal_name, self, '_on_watched_signal')
|
||||
_watched_signals.clear()
|
||||
|
||||
# Returns a list of all the signal names that were emitted by the object.
|
||||
# If the object is not being watched then an empty list is returned.
|
||||
func get_signals_emitted(obj):
|
||||
var emitted = []
|
||||
if(is_watching_object(obj)):
|
||||
for signal_name in _watched_signals[obj]:
|
||||
if(_watched_signals[obj][signal_name].size() > 0):
|
||||
emitted.append(signal_name)
|
||||
|
||||
return emitted
|
Binary file not shown.
|
@ -1,96 +0,0 @@
|
|||
# {
|
||||
# instance_id_or_path1:{
|
||||
# method1:[ [p1, p2], [p1, p2] ],
|
||||
# method2:[ [p1, p2], [p1, p2] ]
|
||||
# },
|
||||
# instance_id_or_path1:{
|
||||
# method1:[ [p1, p2], [p1, p2] ],
|
||||
# method2:[ [p1, p2], [p1, p2] ]
|
||||
# },
|
||||
# }
|
||||
var _calls = {}
|
||||
var _utils = load('res://addons/gut/utils.gd').new()
|
||||
var _lgr = _utils.get_logger()
|
||||
|
||||
func _get_params_as_string(params):
|
||||
var to_return = ''
|
||||
if(params == null):
|
||||
return ''
|
||||
|
||||
for i in range(params.size()):
|
||||
if(params[i] == null):
|
||||
to_return += 'null'
|
||||
else:
|
||||
if(typeof(params[i]) == TYPE_STRING):
|
||||
to_return += str('"', params[i], '"')
|
||||
else:
|
||||
to_return += str(params[i])
|
||||
if(i != params.size() -1):
|
||||
to_return += ', '
|
||||
return to_return
|
||||
|
||||
func add_call(variant, method_name, parameters=null):
|
||||
if(!_calls.has(variant)):
|
||||
_calls[variant] = {}
|
||||
|
||||
if(!_calls[variant].has(method_name)):
|
||||
_calls[variant][method_name] = []
|
||||
|
||||
_calls[variant][method_name].append(parameters)
|
||||
|
||||
func was_called(variant, method_name, parameters=null):
|
||||
var to_return = false
|
||||
if(_calls.has(variant) and _calls[variant].has(method_name)):
|
||||
if(parameters):
|
||||
to_return = _calls[variant][method_name].has(parameters)
|
||||
else:
|
||||
to_return = true
|
||||
return to_return
|
||||
|
||||
func get_call_parameters(variant, method_name, index=-1):
|
||||
var to_return = null
|
||||
var get_index = -1
|
||||
|
||||
if(_calls.has(variant) and _calls[variant].has(method_name)):
|
||||
var call_size = _calls[variant][method_name].size()
|
||||
if(index == -1):
|
||||
# get the most recent call by default
|
||||
get_index = call_size -1
|
||||
else:
|
||||
get_index = index
|
||||
|
||||
if(get_index < call_size):
|
||||
to_return = _calls[variant][method_name][get_index]
|
||||
else:
|
||||
_lgr.error(str('Specified index ', index, ' is outside range of the number of registered calls: ', call_size))
|
||||
|
||||
return to_return
|
||||
|
||||
func call_count(instance, method_name, parameters=null):
|
||||
var to_return = 0
|
||||
|
||||
if(was_called(instance, method_name)):
|
||||
if(parameters):
|
||||
for i in range(_calls[instance][method_name].size()):
|
||||
if(_calls[instance][method_name][i] == parameters):
|
||||
to_return += 1
|
||||
else:
|
||||
to_return = _calls[instance][method_name].size()
|
||||
return to_return
|
||||
|
||||
func clear():
|
||||
_calls = {}
|
||||
|
||||
func get_call_list_as_string(instance):
|
||||
var to_return = ''
|
||||
if(_calls.has(instance)):
|
||||
for method in _calls[instance]:
|
||||
for i in range(_calls[instance][method].size()):
|
||||
to_return += str(method, '(', _get_params_as_string(_calls[instance][method][i]), ")\n")
|
||||
return to_return
|
||||
|
||||
func get_logger():
|
||||
return _lgr
|
||||
|
||||
func set_logger(logger):
|
||||
_lgr = logger
|
|
@ -1,43 +0,0 @@
|
|||
var return_val = null
|
||||
var stub_target = null
|
||||
var target_subpath = null
|
||||
var parameters = null
|
||||
var stub_method = null
|
||||
var call_super = false
|
||||
|
||||
const NOT_SET = '|_1_this_is_not_set_1_|'
|
||||
|
||||
func _init(target=null, method=null, subpath=null):
|
||||
stub_target = target
|
||||
stub_method = method
|
||||
target_subpath = subpath
|
||||
|
||||
func to_return(val):
|
||||
return_val = val
|
||||
call_super = false
|
||||
return self
|
||||
|
||||
func to_do_nothing():
|
||||
return to_return(null)
|
||||
|
||||
func to_call_super():
|
||||
call_super = true
|
||||
return self
|
||||
|
||||
func when_passed(p1=NOT_SET,p2=NOT_SET,p3=NOT_SET,p4=NOT_SET,p5=NOT_SET,p6=NOT_SET,p7=NOT_SET,p8=NOT_SET,p9=NOT_SET,p10=NOT_SET):
|
||||
parameters = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10]
|
||||
var idx = 0
|
||||
while(idx < parameters.size()):
|
||||
if(str(parameters[idx]) == NOT_SET):
|
||||
parameters.remove(idx)
|
||||
else:
|
||||
idx += 1
|
||||
return self
|
||||
|
||||
func to_s():
|
||||
var base_string = str(stub_target, '[', target_subpath, '].', stub_method)
|
||||
if(call_super):
|
||||
base_string += " to call SUPER"
|
||||
else:
|
||||
base_string += str(' with (', parameters, ') = ', return_val)
|
||||
return base_string
|
|
@ -1,162 +0,0 @@
|
|||
# {
|
||||
# inst_id_or_path1:{
|
||||
# method_name1: [StubParams, StubParams],
|
||||
# method_name2: [StubParams, StubParams]
|
||||
# },
|
||||
# inst_id_or_path2:{
|
||||
# method_name1: [StubParams, StubParams],
|
||||
# method_name2: [StubParams, StubParams]
|
||||
# }
|
||||
# }
|
||||
var returns = {}
|
||||
var _utils = load('res://addons/gut/utils.gd').new()
|
||||
var _lgr = _utils.get_logger()
|
||||
|
||||
func _is_instance(obj):
|
||||
return typeof(obj) == TYPE_OBJECT and !obj.has_method('new')
|
||||
|
||||
func _make_key_from_metadata(doubled):
|
||||
var to_return = doubled.__gut_metadata_.path
|
||||
if(doubled.__gut_metadata_.subpath != ''):
|
||||
to_return += str('-', doubled.__gut_metadata_.subpath)
|
||||
return to_return
|
||||
|
||||
# Creates they key for the returns hash based on the type of object passed in
|
||||
# obj could be a string of a path to a script with an optional subpath or
|
||||
# it could be an instance of a doubled object.
|
||||
func _make_key_from_variant(obj, subpath=null):
|
||||
var to_return = null
|
||||
|
||||
match typeof(obj):
|
||||
TYPE_STRING:
|
||||
# this has to match what is done in _make_key_from_metadata
|
||||
to_return = obj
|
||||
if(subpath != null and subpath != ''):
|
||||
to_return += str('-', subpath)
|
||||
TYPE_OBJECT:
|
||||
if(_is_instance(obj)):
|
||||
to_return = _make_key_from_metadata(obj)
|
||||
elif(_utils.is_native_class(obj)):
|
||||
to_return = _utils.get_native_class_name(obj)
|
||||
else:
|
||||
to_return = obj.resource_path
|
||||
return to_return
|
||||
|
||||
func _add_obj_method(obj, method, subpath=null):
|
||||
var key = _make_key_from_variant(obj, subpath)
|
||||
if(_is_instance(obj)):
|
||||
key = obj
|
||||
|
||||
if(!returns.has(key)):
|
||||
returns[key] = {}
|
||||
if(!returns[key].has(method)):
|
||||
returns[key][method] = []
|
||||
|
||||
return key
|
||||
|
||||
# ##############
|
||||
# Public
|
||||
# ##############
|
||||
|
||||
# TODO: This method is only used in tests and should be refactored out. It
|
||||
# does not support inner classes and isn't helpful.
|
||||
func set_return(obj, method, value, parameters=null):
|
||||
var key = _add_obj_method(obj, method)
|
||||
var sp = _utils.StubParams.new(key, method)
|
||||
sp.parameters = parameters
|
||||
sp.return_val = value
|
||||
returns[key][method].append(sp)
|
||||
|
||||
func add_stub(stub_params):
|
||||
var key = _add_obj_method(stub_params.stub_target, stub_params.stub_method, stub_params.target_subpath)
|
||||
returns[key][stub_params.stub_method].append(stub_params)
|
||||
|
||||
# Searches returns for an entry that matches the instance or the class that
|
||||
# passed in obj is.
|
||||
#
|
||||
# obj can be an instance, class, or a path.
|
||||
func _find_stub(obj, method, parameters=null):
|
||||
var key = _make_key_from_variant(obj)
|
||||
var to_return = null
|
||||
|
||||
if(_is_instance(obj)):
|
||||
if(returns.has(obj) and returns[obj].has(method)):
|
||||
key = obj
|
||||
elif(obj.get('__gut_metadata_')):
|
||||
key = _make_key_from_metadata(obj)
|
||||
|
||||
if(returns.has(key) and returns[key].has(method)):
|
||||
var param_idx = -1
|
||||
var null_idx = -1
|
||||
|
||||
for i in range(returns[key][method].size()):
|
||||
if(returns[key][method][i].parameters == parameters):
|
||||
param_idx = i
|
||||
if(returns[key][method][i].parameters == null):
|
||||
null_idx = i
|
||||
|
||||
# We have matching parameter values so return the stub value for that
|
||||
if(param_idx != -1):
|
||||
to_return = returns[key][method][param_idx]
|
||||
# We found a case where the parameters were not specified so return
|
||||
# parameters for that
|
||||
elif(null_idx != -1):
|
||||
to_return = returns[key][method][null_idx]
|
||||
else:
|
||||
_lgr.warn(str('Call to [', method, '] was not stubbed for the supplied parameters ', parameters, '. Null was returned.'))
|
||||
|
||||
return to_return
|
||||
|
||||
# Gets a stubbed return value for the object and method passed in. If the
|
||||
# instance was stubbed it will use that, otherwise it will use the path and
|
||||
# subpath of the object to try to find a value.
|
||||
#
|
||||
# It will also use the optional list of parameter values to find a value. If
|
||||
# the object was stubbed with no parameters than any parameters will match.
|
||||
# If it was stubbed with specific parameter values then it will try to match.
|
||||
# If the parameters do not match BUT there was also an empty parameter list stub
|
||||
# then it will return those.
|
||||
# If it cannot find anything that matches then null is returned.for
|
||||
#
|
||||
# Parameters
|
||||
# obj: this should be an instance of a doubled object.
|
||||
# method: the method called
|
||||
# parameters: optional array of parameter vales to find a return value for.
|
||||
func get_return(obj, method, parameters=null):
|
||||
var stub_info = _find_stub(obj, method, parameters)
|
||||
|
||||
if(stub_info != null):
|
||||
return stub_info.return_val
|
||||
else:
|
||||
return null
|
||||
|
||||
func should_call_super(obj, method, parameters=null):
|
||||
var stub_info = _find_stub(obj, method, parameters)
|
||||
if(stub_info != null):
|
||||
return stub_info.call_super
|
||||
else:
|
||||
# this log message is here because of how the generated doubled scripts
|
||||
# are structured. With this log msg here, you will only see one
|
||||
# "unstubbed" info instead of multiple.
|
||||
_lgr.info('Unstubbed call to ' + method + '::' + str(obj))
|
||||
return false
|
||||
|
||||
|
||||
func clear():
|
||||
returns.clear()
|
||||
|
||||
func get_logger():
|
||||
return _lgr
|
||||
|
||||
func set_logger(logger):
|
||||
_lgr = logger
|
||||
|
||||
func to_s():
|
||||
var text = ''
|
||||
for thing in returns:
|
||||
text += str(thing) + "\n"
|
||||
for method in returns[thing]:
|
||||
text += str("\t", method, "\n")
|
||||
for i in range(returns[thing][method].size()):
|
||||
text += "\t\t" + returns[thing][method][i].to_s() + "\n"
|
||||
return text
|
|
@ -1,152 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# Contains all the results of a single test. Allows for multiple asserts results
|
||||
# and pending calls.
|
||||
# ------------------------------------------------------------------------------
|
||||
class Test:
|
||||
var pass_texts = []
|
||||
var fail_texts = []
|
||||
var pending_texts = []
|
||||
|
||||
func to_s():
|
||||
var pad = ' '
|
||||
var to_return = ''
|
||||
for i in range(fail_texts.size()):
|
||||
to_return += str(pad, 'FAILED: ', fail_texts[i], "\n")
|
||||
for i in range(pending_texts.size()):
|
||||
to_return += str(pad, 'PENDING: ', pending_texts[i], "\n")
|
||||
return to_return
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Contains all the results for a single test-script/inner class. Persists the
|
||||
# names of the tests and results and the order in which the tests were run.
|
||||
# ------------------------------------------------------------------------------
|
||||
class TestScript:
|
||||
var name = 'NOT_SET'
|
||||
#
|
||||
var _tests = {}
|
||||
var _test_order = []
|
||||
|
||||
func _init(script_name):
|
||||
name = script_name
|
||||
|
||||
func get_pass_count():
|
||||
var count = 0
|
||||
for key in _tests:
|
||||
count += _tests[key].pass_texts.size()
|
||||
return count
|
||||
|
||||
func get_fail_count():
|
||||
var count = 0
|
||||
for key in _tests:
|
||||
count += _tests[key].fail_texts.size()
|
||||
return count
|
||||
|
||||
func get_pending_count():
|
||||
var count = 0
|
||||
for key in _tests:
|
||||
count += _tests[key].pending_texts.size()
|
||||
return count
|
||||
|
||||
func get_test_obj(obj_name):
|
||||
if(!_tests.has(obj_name)):
|
||||
_tests[obj_name] = Test.new()
|
||||
_test_order.append(obj_name)
|
||||
return _tests[obj_name]
|
||||
|
||||
func add_pass(test_name, reason):
|
||||
var t = get_test_obj(test_name)
|
||||
t.pass_texts.append(reason)
|
||||
|
||||
func add_fail(test_name, reason):
|
||||
var t = get_test_obj(test_name)
|
||||
t.fail_texts.append(reason)
|
||||
|
||||
func add_pending(test_name, reason):
|
||||
var t = get_test_obj(test_name)
|
||||
t.pending_texts.append(reason)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Summary Class
|
||||
#
|
||||
# This class holds the results of all the test scripts and Inner Classes that
|
||||
# were run.
|
||||
# -------------------------------------------d-----------------------------------
|
||||
var _scripts = []
|
||||
|
||||
func add_script(name):
|
||||
_scripts.append(TestScript.new(name))
|
||||
|
||||
func get_scripts():
|
||||
return _scripts
|
||||
|
||||
func get_current_script():
|
||||
return _scripts[_scripts.size() - 1]
|
||||
|
||||
func add_test(test_name):
|
||||
get_current_script().get_test_obj(test_name)
|
||||
|
||||
func add_pass(test_name, reason = ''):
|
||||
get_current_script().add_pass(test_name, reason)
|
||||
|
||||
func add_fail(test_name, reason = ''):
|
||||
get_current_script().add_fail(test_name, reason)
|
||||
|
||||
func add_pending(test_name, reason = ''):
|
||||
get_current_script().add_pending(test_name, reason)
|
||||
|
||||
func get_test_text(test_name):
|
||||
return test_name + "\n" + get_current_script().get_test_obj(test_name).to_s()
|
||||
|
||||
# Gets the count of unique script names minus the .<Inner Class Name> at the
|
||||
# end. Used for displaying the number of scripts without including all the
|
||||
# Inner Classes.
|
||||
func get_non_inner_class_script_count():
|
||||
var unique_scripts = {}
|
||||
for i in range(_scripts.size()):
|
||||
var ext_loc = _scripts[i].name.find_last('.gd.')
|
||||
if(ext_loc == -1):
|
||||
unique_scripts[_scripts[i].name] = 1
|
||||
else:
|
||||
unique_scripts[_scripts[i].name.substr(0, ext_loc + 3)] = 1
|
||||
return unique_scripts.keys().size()
|
||||
|
||||
func get_totals():
|
||||
var totals = {
|
||||
passing = 0,
|
||||
pending = 0,
|
||||
failing = 0,
|
||||
tests = 0,
|
||||
scripts = 0
|
||||
}
|
||||
|
||||
for s in range(_scripts.size()):
|
||||
totals.passing += _scripts[s].get_pass_count()
|
||||
totals.pending += _scripts[s].get_pending_count()
|
||||
totals.failing += _scripts[s].get_fail_count()
|
||||
totals.tests += _scripts[s]._test_order.size()
|
||||
|
||||
totals.scripts = get_non_inner_class_script_count()
|
||||
|
||||
return totals
|
||||
|
||||
func get_summary_text():
|
||||
var _totals = get_totals()
|
||||
|
||||
var to_return = ''
|
||||
for s in range(_scripts.size()):
|
||||
if(_scripts[s].get_fail_count() > 0 or _scripts[s].get_pending_count() > 0):
|
||||
to_return += _scripts[s].name + "\n"
|
||||
for t in range(_scripts[s]._test_order.size()):
|
||||
var tname = _scripts[s]._test_order[t]
|
||||
var test = _scripts[s].get_test_obj(tname)
|
||||
if(test.fail_texts.size() > 0 or test.pending_texts.size() > 0):
|
||||
to_return += str(' - ', tname, "\n", test.to_s())
|
||||
|
||||
var header = "*** Totals ***\n"
|
||||
header += str(' Scripts: ', get_non_inner_class_script_count(), "\n")
|
||||
header += str(' Tests: ', _totals.tests, "\n")
|
||||
header += str(' Passing asserts: ', _totals.passing, "\n")
|
||||
header += str(' Failing asserts: ',_totals.failing, "\n")
|
||||
header += str(' Pending: ', _totals.pending, "\n")
|
||||
|
||||
return to_return + "\n" + header
|
1173
addons/gut/test.gd
1173
addons/gut/test.gd
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue