Add/update more files

This commit is contained in:
Leroy Hopson 2020-05-10 22:56:49 +12:00
parent 3307231b65
commit 0769592a1b
44 changed files with 4188 additions and 362 deletions

BIN
.test.txt.swp Normal file

Binary file not shown.

View file

@ -0,0 +1,127 @@
# 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()
# 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 fg & 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
func has_extended_attrs() -> int:
return 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:
var underline_style = UnderlineStyle.NONE
var underline_color: int = -1
func _init():
underline_style

View file

@ -0,0 +1,141 @@
# 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 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()
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}
func setup_tab_stops(i = null) -> void:
if i == null:
return
if not tabs.get(i):
i = prev_stop(i)
else:
tabs = {}
i = 0
while i < _cols:
tabs[i] = true
i += _options_service.options.tab_stop_width
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

View file

@ -0,0 +1,247 @@
# 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:
return _data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT
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 i in range(n):
set_cell(pos + i, 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)
func get_trimmed_length() -> int:
for i in range(length - 1, 0, -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 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

View file

@ -0,0 +1,55 @@
# 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
# TODO
#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)

View file

@ -0,0 +1,68 @@
# 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()]

View file

@ -0,0 +1,93 @@
# 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
}

View file

@ -0,0 +1,75 @@
# 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
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
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
func _init(max_length):
self.max_length = max_length
_array = []
_array.resize(max_length)
_start_index = 0
func get_el(index: int):
return _array[_get_cyclic_index(index)]
func set_el(index: int, value) -> void:
_array[_get_cyclic_index(index)] = value
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
func _get_cyclic_index(index: int) -> int:
return _start_index + index % max_length

View file

@ -0,0 +1,108 @@
# 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(r, g, b))
# Generate greys (232-255)
for i in range(0, 24):
var c = 8 + i * 10
colors.append(Color(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()

View file

@ -0,0 +1,24 @@
# 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 = {
# British character set
# ESC (A
# Reference: http://vt100.net/docs/vt220-rm/table2-5.html
'A': {
'#': '£'
},
# United States character set
# ESC (B
'B': null,
}
# The default character set, US.
const DEFAULT_CHARSET = CHARSETS['B']

View file

@ -3,4 +3,5 @@
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.ttf" type="DynamicFontData" id=1] [ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.ttf" type="DynamicFontData" id=1]
[resource] [resource]
size = 20
font_data = ExtResource( 1 ) font_data = ExtResource( 1 )

View file

@ -28,22 +28,19 @@ static func utf32_to_utf8(codepoint: int):
return utf8 return utf8
# Convert UTF32 codepoint into a String.
static func string_from_codepoint(codepoint: int):
var utf8 = utf32_to_utf8(codepoint)
return utf8.get_string_from_utf8()
# Covert UTF32 char codes into a String. # Covert UTF32 char codes into a String.
# Basically the same as `string_from_codepoint` but for multiple codepoints # Basically the same as `char` but for multiple codepoints
# in a loop (which is a lot faster). # in a loop (which is a lot faster).
static func utf32_to_string(data: Array, start: int = 0, end: int = -1): static func utf32_to_string(data: Array, start: int = 0, end: int = -1):
if end == -1: if end == -1:
end = data.size() end = data.size()
var result = '' var result = ''
for i in range(start, end): for i in range(start, end):
result += string_from_codepoint(data[i]) result += char(data[i])
return result return result
# Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints. # Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.
class Utf8ToUtf32: class Utf8ToUtf32:
var interim = PoolByteArray() var interim = PoolByteArray()

File diff suppressed because it is too large Load diff

View file

@ -244,7 +244,7 @@ func parse(data: Array, length: int):
handlers.invert() handlers.invert()
for handler in handlers: for handler in handlers:
# undefined or true means success and to stop bubbling # undefined or true means success and to stop bubbling
if handler['target'].call(handler['method'], params.to_array()): if handler['target'].call(handler['method'], params):
continue continue
handlers.invert() handlers.invert()
if handlers.empty(): if handlers.empty():

View file

@ -53,6 +53,17 @@ func _init(max_length: int = 32, max_sub_params_length: int = 32):
sub_params.resize(max_sub_params_length) sub_params.resize(max_sub_params_length)
sub_params_idx.resize(max_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): func add_param(value: int):
digit_is_sub = false digit_is_sub = false
if length >= _max_length: if length >= _max_length:
@ -78,7 +89,6 @@ func add_sub_param(value: int):
sub_params_idx[length - 1] += 1 sub_params_idx[length - 1] += 1
func add_digit(value: int): func add_digit(value: int):
print("adding digit: ", value, " is sub: ", digit_is_sub)
var _length = sub_params_length if digit_is_sub else length 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): if _reject_digits or (not _length) or (digit_is_sub and _reject_sub_digits):
return return
@ -86,6 +96,11 @@ func add_digit(value: int):
var cur = store[_length - 1] var cur = store[_length - 1]
store[_length - 1] = min(cur * 10 + value, MAX_VALUE) if ~cur else value store[_length - 1] = min(cur * 10 + value, MAX_VALUE) if ~cur else value
func size():
return params.size()
func to_array(): func to_array():
var res = [] var res = []
for i in range(length): for i in range(length):

View file

@ -1,5 +1,5 @@
# Copyright (c) 2020 The GodotXterm authors.
# Copyright (c) 2019 The xterm.js authors. All rights reserved. # Copyright (c) 2019 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT # License MIT
extends Reference extends Reference

View file

@ -1,3 +1,5 @@
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
# License MIT
tool tool
extends EditorPlugin extends EditorPlugin

View file

@ -0,0 +1,250 @@
# Copyright (c) 2017 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 AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
const CanvasRenderingContext2D = preload("res://addons/godot_xterm/renderer/canvas_rendering_context_2d.gd")
const Attributes = Constants.Attributes
# TODO: Something about these consts and atlas
const INVERTED_DEFAULT_COLOR = Color(0, 0, 0, 1)
const DEFAULT_COLOR = Color(1, 1, 1, 0)
var _container: Node
var id: String
var z_index: int
var _alpha: bool
var _colors
var _renderer_id: int
var _buffer_service
var _options_service
var _ctx: CanvasRenderingContext2D
var _scaled_char_width: int = 0
var _scaled_char_height: int = 0
var _scaled_cell_width: int = 0
var _scaled_cell_height: int = 0
var _scaled_char_left: int = 0
var _scaled_char_top: int = 0
var _char_atlas
# An object that's reused when drawing glyphs in order to reduce GC.
class GlyphIdentifier:
extends Reference
var chars = ''
var code = 0
var bg = 0
var fg = 0
var bold = false
var dim = false
var italic = false
var _current_glyph_identifier = GlyphIdentifier.new()
func _init(container: Node, id: String, z_index: int, alpha: bool,
colors: Dictionary, renderer_id: int, buffer_service, options_service):
_container = container
self.id = id
self.z_index = z_index
_alpha = alpha
_colors = colors
_renderer_id = renderer_id
_buffer_service = buffer_service
_options_service = options_service
_ctx = CanvasRenderingContext2D.new()
_ctx.z_index = z_index
_container.add_child(_ctx)
func on_grid_changed(start_row: int, end_row: int) -> void:
pass
func resize(dim) -> void:
_scaled_cell_width = dim.scaled_cell_width
_scaled_cell_height = dim.scaled_cell_height
_scaled_char_width = dim.scaled_char_width
_scaled_char_height = dim.scaled_char_height
_scaled_char_left = dim.scaled_char_left
_scaled_char_top = dim.scaled_char_top
#_canvas_width = dim.scaled_canvas_width
#_canvas_height = dim.scaled_canvas_height
#this._canvas.style.width = `${dim.canvasWidth}px`;
#this._canvas.style.height = `${dim.canvasHeight}px`;
func _fill_cells(x: int, y: int, width: int, height: int) -> void:
_ctx.fill_rect(Rect2(x * _scaled_cell_width, y * _scaled_cell_height,
width * _scaled_cell_width, height * _scaled_cell_height))
func _clear_cells(x: int, y: int, width: int, height: int) -> void:
var scaled = Rect2(x * _scaled_cell_width, y * _scaled_cell_height,
width * _scaled_cell_width, height * _scaled_cell_height)
if _alpha:
_ctx.clear_rect(scaled)
else:
_ctx.fill_style = _colors.background
_ctx.fill_rect(scaled)
func _draw_chars(cell, x, y) -> void:
# TODO
#var contrast_color = _get_contrast_color(cell)
var contrast_color = null
# skip cache right away if we draw in RGB
# Note: to avoid bad runtime JoinedCellData will be skipped
# in the cache handler itself (atlasDidDraw == false) and
# fall through to uncached later down below
if contrast_color or cell.is_fg_rgb() or cell.is_bg_rgb():
_draw_uncached_chars(cell, x, y, contrast_color)
return
var fg
var bg
if cell.is_inverse():
fg = INVERTED_DEFAULT_COLOR if cell.is_bg_default() else cell.get_bg_color()
bg = INVERTED_DEFAULT_COLOR if cell.is_fg_default() else cell.get_fg_color()
else:
bg = DEFAULT_COLOR if cell.is_bg_default() else cell.get_bg_color()
fg = DEFAULT_COLOR if cell.is_fg_default() else cell.get_fg_color()
var draw_in_bright_color = _options_service.options.draw_bold_text_in_bright_colors and cell.is_bold() and fg < 8
fg = Color(fg as int + 8) if draw_in_bright_color else 0
_current_glyph_identifier.chars = cell.get_chars() if cell.get_chars() else Constants.WHITESPACE_CELL_CHAR
_current_glyph_identifier.code = cell.get_code() if cell.get_code() else Constants.WHITESPACE_CELL_CODE
_current_glyph_identifier.bg = bg
_current_glyph_identifier.fg = fg
_current_glyph_identifier.bold = cell.is_bold() as bool
_current_glyph_identifier.dim = cell.is_dim() as bool
_current_glyph_identifier.italic = cell.is_italic() as bool
var atlas_did_draw = _char_atlas and _char_atlas.draw(_ctx,
_current_glyph_identifier, x * _scaled_cell_width + _scaled_char_left,
y * _scaled_cell_width, _scaled_char_top)
if not atlas_did_draw:
_draw_uncached_chars(cell, x, y)
# Draws one or more charaters at one or more cells. The character(s) will be
# clipped to ensure that they fit with the cell(s), including the cell to the
# right if the last character is a wide character.
func _draw_uncached_chars(cell, x: int, y: int, fg_override = null) -> void:
_ctx.save()
_ctx.font = _get_font(cell.is_bold() as bool, cell.is_italic() as bool)
if cell.is_inverse():
if cell.is_bg_default():
_ctx.fill_style = _colors.background
elif cell.is_bg_rgb():
_ctx.fill_style = AttributeData.to_color_rgb(cell.get_bg_color())
else:
var bg = cell.get_bg_color()
if _options_service.options.draw_bold_text_in_bright_colors and cell.is_bold() and bg < 8:
bg += 8
_ctx.fill_style = _colors.ansi[bg]
else:
if cell.is_fg_default():
_ctx.fill_style = _colors.foreground
elif cell.is_fg_rgb():
_ctx.fill_style = AttributeData.to_color_rgb(cell.get_fg_color())
else:
var fg = cell.get_fg_color()
if _options_service.options.draw_bold_text_in_bright_colors and cell.is_bold() and fg < 8:
fg += 8
_ctx.fill_style = _colors.ansi[fg]
#_clip_row(y)
# Apply alpha to dim the character
if cell.is_dim():
pass
#_ctx.global_alpha = DIM_OPACITY
# Draw the character
_ctx.fill_text(cell.get_chars(), x * _scaled_cell_width + _scaled_char_left,
y * _scaled_cell_height + _scaled_char_top + _scaled_char_height / 2)
_ctx.restore()
func _get_font(is_bold: bool, is_italic: bool) -> Font:
var font_family = _options_service.options.font_family
if is_bold and is_italic and font_family.bold_italic:
return font_family.bold_italic
elif is_bold and font_family.bold:
return font_family.bold
elif is_italic and font_family.italic:
return font_family.italic
else:
return font_family.regular
func _get_contrast_color(cell):
if _options_service.options.minimum_contrast_ratio == 1:
return null
var adjusted_color = _colors.contrast_cache.get_color(cell.bg, cell.fg)
if adjusted_color != null:
return adjusted_color
var fg_color = cell.get_fg_color()
var fg_color_mode = cell.get_fg_color_mode()
var bg_color = cell.get_bg_color()
var bg_color_mode = cell.get_bg_color_mode()
var is_inverse = cell.is_inverse() as bool
var is_bold = cell.is_bold() as bool
if is_inverse:
var temp = fg_color
fg_color = bg_color
bg_color = temp
var temp2 = fg_color_mode
fg_color_mode = bg_color_mode
bg_color_mode = temp2
var bg_rgba = _resolve_background_rgba(bg_color_mode, bg_color, is_inverse)
var fg_rgba = _resolve_foreground_rgba(fg_color_mode, fg_color, is_inverse, is_bold)
# TODO
#var result = rgba.ensure_contrast_ratio(bg_rgba, fg_rgba, _options_service.options.minimum_contrast_ratio)
func _resolve_background_rgba(bg_color_mode: int, bg_color: int, inverse: bool) -> int:
match bg_color_mode:
Attributes.CM_P16, Attributes.CM_P256:
return _colors.ansi[bg_color].rgba
Attributes.CM_RGB:
return bg_color << 8
Attributes.CM_DEFAULT, _:
if inverse:
return _colors.foreground.rgba
else:
return _colors.background.rgba
func _resolve_foreground_rgba(fg_color_mode: int, fg_color: int, inverse: bool, bold: bool):
match fg_color_mode:
Attributes.CM_P16, Attributes.CM_P256:
if _options_service.options.draw_bold_text_in_bright_colors and bold and fg_color < 8:
return _colors.ansi[fg_color].rgba
Attributes.CM_RGB:
return fg_color << 8
Attributes.CM_DEFAULT, _:
if inverse:
return _colors.background.rgba
else:
return _colors.foreground.rgba

View file

@ -0,0 +1,74 @@
# 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

View file

@ -0,0 +1,46 @@
# 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

View file

@ -0,0 +1,118 @@
# Copyright (c) 2017 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends Reference
const CharacterJoinerRegistry = preload("res://addons/godot_xterm/renderer/character_joiner_registry.gd")
const TextRenderLayer = preload("res://addons/godot_xterm/renderer/text_render_layer.gd")
signal redraw_requested
signal options_changed
signal grid_changed(start, end)
var _id: int
var _render_layers: Array
var _device_pixel_ratio: float
var _character_joiner_registry
var _colors
var _container
var _buffer_service
var _options_service
var _char_size_service
var dimensions
func _init(colors, container: Node, buffer_service, options_service):
_id = get_instance_id()
_colors = colors
_container = container
_buffer_service = buffer_service
_options_service = options_service
var allow_transparency = _options_service.options.allow_transparency
_character_joiner_registry = CharacterJoinerRegistry.new(_buffer_service)
_render_layers = [
TextRenderLayer.new(_container, 0, _colors, _character_joiner_registry,
allow_transparency, _id, _buffer_service, _options_service)
]
# Connect render layers to our signals.
for layer in _render_layers:
self.connect("options_changed", layer, "on_options_changed")
self.connect("grid_changed", layer, "on_grid_changed")
dimensions = {
"scaled_char_width": 0,
"scaled_char_height": 0,
"scaled_cell_width": 0,
"scaled_cell_height": 0,
"scaled_char_left": 0,
"scaled_char_top": 0,
"scaled_canvas_width": 0,
"scaled_canvas_height": 0,
"canvas_width": 0,
"canvas_height": 0,
"actual_cell_width": 0,
"actual_cell_height": 0,
}
_device_pixel_ratio = OS.get_screen_dpi()
_update_dimensions()
emit_signal("options_changed")
func on_resize(cols, rows):
# Update character and canvas dimensions
_update_dimensions()
# Resize all render layers
for layer in _render_layers:
layer.resize(dimensions)
func refresh_rows(start: int, end: int) -> void:
emit_signal("grid_changed", start, end)
# 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)
dimensions.scaled_char_width = char_width
dimensions.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.
dimensions.scaled_cell_height = floor(dimensions.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.
dimensions.scaled_char_top = 0 if _options_service.options.line_height == 1 else \
round((dimensions.scaled_cell_height - dimensions.scaled_char_height) / 2)
# Calculate the scaled cell width, taking the letter_spacing into account.
dimensions.scaled_cell_width = dimensions.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.
dimensions.scaled_char_left = floor(_options_service.options.letter_spacing / 2)

View file

@ -0,0 +1,191 @@
# Copyright (c) 2017 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends "res://addons/godot_xterm/renderer/base_render_layer.gd"
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
const CharacterJoinerRegistry = preload("res://addons/godot_xterm/renderer/character_joiner_registry.gd")
const JoinedCellData = CharacterJoinerRegistry.JoinedCellData
const Content = Constants.Content
var _state
var _character_width: int = 0
var _character_font: DynamicFont = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres")
var _character_overlap_cache: Dictionary = {}
var _character_joiner_registry
var _work_cell = CellData.new()
func _init(container: Node, z_index: int, colors, character_joiner_registry,
alpha: bool, renderer_id: int, buffer_service, options_service).(container,
'text', z_index, alpha, colors, renderer_id, buffer_service, options_service):
_state = null #TODO what?
_character_joiner_registry = character_joiner_registry
func on_grid_changed(first_row: int, last_row: int) -> void:
_clear_cells(0, first_row, _buffer_service.cols, last_row - first_row + 1)
_draw_background(first_row, last_row)
_draw_foreground(first_row, last_row)
# Finally draw everything that has been queued in the draw buffer.
_ctx.update()
func on_options_changed() -> void:
pass
func _cells(first_row: int, last_row: int, joiner_registry = null) -> Array:
var cells = []
for y in range(first_row, last_row + 1):
var row = y + _buffer_service.buffer.ydisp
var line = _buffer_service.buffer.lines.get_el(row)
var joined_ranges = joiner_registry.get_joined_characters(row) if joiner_registry else []
for x in range(_buffer_service.cols):
line.load_cell(x, _work_cell)
var cell = _work_cell
# If true, indicates that the current character(s) to draw were joined.
var is_joined = false
var last_char_x = x
# The character to the left is a wide character, drawing is owned by
# the char at x-1
if cell.get_width() == 0:
continue
# Process any joined character range as needed. Because of how the
# ranges are produced, we know that they are valid for the characters
# and attributes of our input.
if not joined_ranges.empty() and x == joined_ranges[0][0]:
is_joined = true
var r = joined_ranges.pop_front()
# We already know the exact start and end column of the joined
# range, so we get the string and width representing it directly
cell = JoinedCellData.new(_work_cell,
line.trans_late_to_string(true, r[0], r[1]), r[1] - r[0])
# Skip over the cells occupied by this range in the loop
last_char_x = r[1] - 1
# If the character is an overlapping char and the character to the
# right is a space, take ownership of the cell to the right. We skip
# this check for joined characters because their rendering likely won't
# yield the same result as rendering the last character individually.
if not is_joined and _is_overlapping(cell):
if last_char_x < line.length - 1 and line.get_codepoint(last_char_x + 1) == Constants.NULL_CELL_CODE:
# patch width to 2
cell.content &= ~Content.WIDTH_MASK
cell.content |= 2 << Content.WIDTH_SHIFT
# Append a new instance of cell, as we wil reuse the current instance.
cells.append({"cell": CellData.from_char_data(cell.get_as_char_data()),
"x": x, "y": y})
x = last_char_x
return cells
func _draw_background(first_row: int, last_row: int) -> void:
var ctx = _ctx
var cols = _buffer_service.cols
var start_x = 0
var start_y = 0
var prev_fill_style = null
ctx.save()
for c in _cells(first_row, last_row, null):
var cell = c.cell
var x = c.x
var y = c.y
# libvte and xterm draw the background (but not foreground) of invisible characters,
# so we should too.
var next_fill_style = null # null represents the default background color
if cell.is_inverse():
if cell.is_fg_default():
next_fill_style = _colors.foreground
elif cell.is_fg_rgb():
next_fill_style = cell.get_fg_color() # TODO: Figure out how to convert this to Color()
else:
next_fill_style = _colors.ansi[cell.get_fg_color()]
elif cell.is_bg_rgb():
next_fill_style = cell.get_bg_color() # TODO: Figure out how to convert this to Color()
elif cell.is_bg_palette():
next_fill_style = _colors.ansi[cell.get_bg_color()]
if prev_fill_style == null:
# This is either the first iteration, or the default background was set. Either way, we
# don't need to draw anything.
start_x = x
start_y = y
if y != start_y:
# our row changed, draw the previous row
ctx.fill_style = prev_fill_style if prev_fill_style else Color()
_fill_cells(start_x, start_y, cols - start_x, 1)
start_x = x
start_y = y
elif prev_fill_style != next_fill_style:
# our color changed, draw the previous characters in this row
ctx.fill_style = prev_fill_style if prev_fill_style else Color()
start_x = x
start_y = y
prev_fill_style = next_fill_style
# flush the last color we encountered
if prev_fill_style != null:
ctx.fill_style = prev_fill_style
_fill_cells(start_x, start_y, cols - start_x, 1)
ctx.restore()
func _draw_foreground(first_row: int, last_row: int) -> void:
for c in _cells(first_row, last_row, _character_joiner_registry):
var cell = c.cell
var x = c.x
var y = c.y
if cell.is_invisible():
return
_draw_chars(cell, x, y)
func _is_overlapping(cell) -> bool:
# Only single cell characters can be overlapping, rendering issues can
# occur without this check
if cell.get_width() != 1:
return false
var chars = cell.get_chars()
# Deliver from cache if available
if _character_overlap_cache.has(chars):
return _character_overlap_cache[chars]
# Setup the font
_ctx.save()
_ctx.font = _character_font
# Measure the width of the character, but floor it
# because that is what the renderer does when it calculates
# the character dimensions wer are comparing against
var overlaps = floor(_ctx.measure_text(chars).width) > _character_width
# Restore the original context
_ctx.restore()
# Cache and return
_character_overlap_cache[chars] = overlaps
return overlaps

View file

@ -0,0 +1,39 @@
# 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
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
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 _buffer_activated(active_buffer, inactive_buffer):
emit_signal("buffer_activated", active_buffer, inactive_buffer)

View file

@ -0,0 +1,30 @@
# 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

View file

@ -0,0 +1,27 @@
# 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()

View file

@ -0,0 +1,53 @@
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends Reference
class TerminalOptions:
var cols: int
var rows: int
var cursor_blink: bool
var cursor_style
var cursor_width: int
var bell_sound
var bell_style
var draw_bold_text_in_bright_colors: bool
var fast_scroll_modifier
var fast_scroll_sensitivity: int
var font_family: Dictionary
var font_size: int
var font_weight: String
var font_weight_bold: String
var line_height: float
var link_tooltip_hover_duration: int
var letter_spacing: float
var log_level
var scrollback: int
var scroll_sensitivity: int
var screen_reader_mode: bool
var mac_option_is_meta: bool
var mac_option_click_forces_selection: bool
var minimum_contrast_ratio: float
var disable_stdin: bool
var allow_proposed_api: bool
var allow_transparency: bool
var tab_stop_width: int
var colors: Dictionary
var right_click_selects_word
var renderer_type
var window_options: Dictionary
var windows_mode: bool
var word_separator: String
var convert_eol: bool
var term_name: String
var cancel_events: bool
signal option_changed
var options
func _init(options):
self.options = options

View file

@ -1,16 +1,42 @@
# Copyright (c) 2020 The GodotXterm authors. All rights reserved. # Copyright (c) 2020 The GodotXterm authors. All rights reserved.
# License MIT # 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 tool
extends Control extends Control
signal data_sent(data) 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 Const = preload("res://addons/godot_xterm/Constants.gd")
const Constants = preload("res://addons/godot_xterm/parser/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 Parser = preload("res://addons/godot_xterm/parser/escape_sequence_parser.gd")
const Buffer = preload("res://addons/godot_xterm/buffer.gd")
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd") const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
const Renderer = preload("res://addons/godot_xterm/renderer/renderer.gd")
const ColorManager = preload("res://addons/godot_xterm/color_manager.gd")
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
const SourceCodeProRegular = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres") 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 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 SourceCodeProItalic = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_italic.tres")
@ -26,342 +52,200 @@ const LEFT_BRACKET = 91
const ENTER = 10 const ENTER = 10
const BACKSPACE_ALT = 127 const BACKSPACE_ALT = 127
export (Font) var normal_font = SourceCodeProRegular setget _set_normal_font # TODO: Move me somewhere else.
export (Font) var bold_font = SourceCodeProBold setget _set_bold_font enum BellStyle {
export (Font) var italic_font = SourceCodeProItalic setget _set_italics_font NONE
export (Font) var bold_italic_font = SourceCodeProBoldItalic setget _set_bold_italics_font }
var buffer
var alternate_buffer signal output(data)
var parser
export var cols = 80
export var rows = 24
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 font_weight = 'normal' # Enum?
export var font_weight_bold = 'bold' # Enum?
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(0, 0, 0)
}
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 decoder
var cols = 80 var parser
var rows = 24 var _buffer_service
var cell: Vector2 var _core_service
var _charset_service
# font flags var _input_handler
export(int, FLAGS, var _render_service
"Bold", var _color_manager
"Italic", # Not xterm-256color var _scaled_char_width
"Underlined", var _scaled_char_height
"Blink", var _scaled_cell_width
"Inverse", var _scaled_cell_height
"Invisible", var _scaled_char_top
"Strikethrough" # Not xterm-256color var _scaled_char_left
) var font_flags = Const.FONT_NORMAL var _work_cell = CellData.new()
func _init():
pass
func _set_normal_font(font: Font) -> void:
normal_font = font
_calculate_cell_size()
func _set_bold_font(font: Font) -> void:
bold_font = font
_calculate_cell_size()
func _set_italics_font(font: Font) -> void:
italic_font = font
_calculate_cell_size()
func _set_bold_italics_font(font: Font) -> void:
bold_italic_font = font
_calculate_cell_size()
func _calculate_cell_size() -> void:
var x = 0.0
var y = 0.0
var fonts = [normal_font, bold_font, italic_font, bold_italic_font]
for font in fonts:
if not font:
continue
var size = font.get_string_size("W")
x = max(x, size.x)
y = max(y, size.y)
cell.x = x
cell.y = y
func _ready(): func _ready():
_calculate_cell_size() var options = OptionsService.TerminalOptions.new()
var rect = get_rect() options.cols = cols
var rs = rect_size options.rows = rows
cols = (rect_size.x / cell.x) as int options.font_family = font_family
rows = (rect_size.y / cell.y) as int options.line_height = line_height
options.screen_reader_mode = screen_reader_mode
options.window_options = window_options
options.convert_eol = convert_eol
decoder = Decoder.Utf8ToUtf32.new() options_service = OptionsService.new(options)
options_service.connect("option_changed", self, "_update_options")
buffer = Buffer.new(rows, cols) _buffer_service = BufferService.new(options_service)
alternate_buffer = Buffer.new(rows, cols, true) _core_service = CoreService.new()
_charset_service = CharsetService.new()
parser = Parser.new()
# Print handler # Register input handler and connect signals.
parser.set_print_handler(buffer, "insert_at_cursor") _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")
# Execute handlers _color_manager = ColorManager.new()
parser.set_execute_handler(C0.BEL, self, 'bell') _color_manager.set_theme(colors)
parser.set_execute_handler(C0.LF, buffer, 'line_feed') _render_service = Renderer.new(_color_manager.colors, self, _buffer_service, options_service)
parser.set_execute_handler(C0.VT, buffer, 'line_feed')
parser.set_execute_handler(C0.FF, buffer, 'line_feed')
parser.set_execute_handler(C0.CR, buffer, 'carriage_return')
parser.set_execute_handler(C0.BS, buffer, 'backspace')
parser.set_execute_handler(C0.HT, buffer, 'insert_tab');
parser.set_execute_handler(C0.SO, self, 'shift_out')
parser.set_execute_handler(C0.SI, self, 'shift_in')
parser.set_execute_handler(C1.IND, self, 'index')
parser.set_execute_handler(C1.NEL, self, 'next_line')
parser.set_execute_handler(C1.HTS, self, 'tab_set')
# CSI handlers connect("resized", self, "_update_dimensions")
parser.set_csi_handler({'final': '@'}, self, 'insert_chars') _update_dimensions()
parser.set_csi_handler({'intermediates': ' ', 'final': '@'}, self, 'scroll_left')
parser.set_csi_handler({'final': 'A'}, self, 'cursor_up')
parser.set_csi_handler({'intermediates': ' ', 'final': 'A'}, self, 'scroll_right')
parser.set_csi_handler({'final': 'B'}, self, 'cursor_down')
parser.set_csi_handler({'final': 'C'}, self, 'cursor_forward')
parser.set_csi_handler({'final': 'D'}, self, 'cursor_backward')
parser.set_csi_handler({'final': 'E'}, self, 'cursor_nextLine')
parser.set_csi_handler({'final': 'F'}, self, 'cursor_precedingLine')
parser.set_csi_handler({'final': 'G'}, self, 'cursor_charAbsolute')
parser.set_csi_handler({'final': 'H'}, buffer, 'cursor_position')
parser.set_csi_handler({'final': 'I'}, self, 'cursor_forward_tab')
parser.set_csi_handler({'final': 'J'}, self, 'erase_in_display')
parser.set_csi_handler({'prefix': '?', 'final': 'J'}, self, 'erase_in_display')
parser.set_csi_handler({'final': 'K'}, self, 'erase_in_line')
parser.set_csi_handler({'prefix': '?', 'final': 'K'}, self, 'erase_in_line')
parser.set_csi_handler({'final': 'L'}, self, 'insert_lines')
parser.set_csi_handler({'final': 'M'}, self, 'delete_lines')
parser.set_csi_handler({'final': 'P'}, self, 'delete_chars')
parser.set_csi_handler({'final': 'S'}, self, 'scroll_up')
parser.set_csi_handler({'final': 'T'}, self, 'scroll_down')
parser.set_csi_handler({'final': 'X'}, self, 'erase_chars')
parser.set_csi_handler({'final': 'Z'}, self, 'cursor_backward_tab')
parser.set_csi_handler({'final': '`'}, self, 'char_pos_absolute')
parser.set_csi_handler({'final': 'a'}, self, 'h_position_relative')
parser.set_csi_handler({'final': 'b'}, self, 'repeat_preceding_character')
parser.set_csi_handler({'final': 'c'}, self, 'send_device_attributes_primary')
parser.set_csi_handler({'prefix': '>', 'final': 'c'}, self, 'send_device_attributes_secondary')
parser.set_csi_handler({'final': 'd'}, self, 'line_pos_absolute')
parser.set_csi_handler({'final': 'e'}, self, 'v_position_relative')
parser.set_csi_handler({'final': 'f'}, self, 'h_v_position')
parser.set_csi_handler({'final': 'g'}, self, 'tab_clear')
parser.set_csi_handler({'final': 'h'}, self, 'set_mode')
parser.set_csi_handler({'prefix': '?', 'final': 'h'}, self, 'set_mode_private')
parser.set_csi_handler({'final': 'l'}, self, 'reset_mode')
parser.set_csi_handler({'prefix': '?', 'final': 'l'}, self, 'reset_mode_private')
parser.set_csi_handler({'final': 'm'}, self, 'char_attributes')
parser.set_csi_handler({'final': 'n'}, self, 'device_status')
parser.set_csi_handler({'prefix': '?', 'final': 'n'}, self, 'device_status_private')
parser.set_csi_handler({'intermediates': '!', 'final': 'p'}, self, 'soft_reset')
parser.set_csi_handler({'intermediates': ' ', 'final': 'q'}, self, 'set_cursor_style')
parser.set_csi_handler({'final': 'r'}, self, 'set_scroll_region')
parser.set_csi_handler({'final': 's'}, self, 'save_cursor')
parser.set_csi_handler({'final': 't'}, self, 'window_options')
parser.set_csi_handler({'final': 'u'}, self, 'restore_cursor')
parser.set_csi_handler({'intermediates': '\'', 'final': '}'}, self, 'insert_columns')
parser.set_csi_handler({'intermediates': '\'', 'final': '~'}, self, 'delete_columns')
func print(data, start, end):
print(data.substr(start, end))
func bell():
print("The bell signal was emited!")
func line_feed(): func _refresh_rows(start_row = 0, end_row = 0):
pass # Not optimized, just draw
update()
func carriage_return():
print("carriage return!")
func backspace():
print("backspace!")
pass
func tab():
pass
func shift_out():
pass
func shift_in():
pass
func index():
pass
func next_line():
pass
func tab_set():
pass
func insert_chars(params):
pass
func scroll_left(params):
pass
func cursor_up(params):
pass
func scroll_right(params):
pass
func cursor_down(params):
pass
func cursor_forward(params):
pass
func cursor_backward(params):
pass
func cursor_next_line(params):
pass
func cursor_preceding_line(params):
pass
func cursor_char_absolute(params):
pass
func cursor_position(params):
pass
func cursor_forward_tab(params):
pass
func erase_in_display(params):
pass
func erase_in_line(params):
pass
func insert_lines(params):
pass
func delete_lines(params):
pass
func delete_chars(params):
pass
func scroll_up(params):
pass
func scroll_down(params):
pass
func erase_chars(params):
pass
func cursor_backward_tab(params):
pass
func char_pos_absolute(params):
pass
func h_position_relative(params):
pass
func repeat_preceding_character(params):
pass
func send_device_attributes_primary(params):
pass
func send_device_attributes_secondary(params):
pass
func line_pos_absolute(params):
pass
func v_position_relative(params):
pass
func h_v_position(params):
pass
func tab_clear(params):
pass
func set_mode(params):
pass
func set_mode_private(params):
pass
func reset_mode(params):
pass
func char_attributes(params):
pass
func device_status(params):
pass
func device_status_private(params):
pass
func soft_reset(params):
pass
func set_cursor_style(params):
pass
func set_scroll_region(params):
pass
func save_cursor(params):
pass
func window_options(params):
pass
func restore_cursor(params):
pass
func insert_columns(params):
pass
func delete_columns(params):
pass
func _input(event): func _input(event):
if event is InputEventKey and event.pressed: if event is InputEventKey and event.pressed:
var data = PoolByteArray([])
accept_event() accept_event()
# TODO: Handle more of these. # TODO: Handle more of these.
if (event.control and event.scancode == KEY_C): if (event.control and event.scancode == KEY_C):
send_data(PoolByteArray([3])) data.append(3)
elif event.unicode: elif event.unicode:
send_data(PoolByteArray([event.unicode])) data.append(event.unicode)
elif event.scancode == KEY_ENTER: elif event.scancode == KEY_ENTER:
send_data(PoolByteArray([ENTER])) data.append(ENTER)
elif event.scancode == KEY_BACKSPACE: elif event.scancode == KEY_BACKSPACE:
send_data(PoolByteArray([BACKSPACE_ALT])) data.append(BACKSPACE_ALT)
elif event.scancode == KEY_ESCAPE: elif event.scancode == KEY_ESCAPE:
send_data(PoolByteArray([27])) data.append(27)
elif event.scancode == KEY_TAB: elif event.scancode == KEY_TAB:
send_data(PoolByteArray([9])) data.append(9)
elif OS.get_scancode_string(event.scancode) == "Shift": elif OS.get_scancode_string(event.scancode) == "Shift":
pass pass
elif OS.get_scancode_string(event.scancode) == "Control": elif OS.get_scancode_string(event.scancode) == "Control":
pass pass
else: else:
push_warning('Unhandled input. scancode: ' + str(OS.get_scancode_string(event.scancode))) pass
#push_warning('Unhandled input. scancode: ' + str(OS.get_scancode_string(event.scancode)))
emit_signal("output", data)
func send_data(data: PoolByteArray): func write(data, callback_target = null, callback_method: String = ''):
emit_signal("data_sent", data) _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)
func _draw(): func _draw():
# Draw the terminal background # Draw the background and foreground
draw_rect(get_rect(), Color(0.0, 0.5, 0.0)) var buffer = _buffer_service.buffer
for y in range(buffer.ybase, rows):
# Naive method. Draw the entire buffer starting with row 0. var line = buffer.lines.get_el(y)
for row in range(buffer.rows.size()): for x in line.length:
#print("Doing the thing for row: ", row) line.load_cell(x, _work_cell)
# Draw each CharacterData. draw_rect(Rect2(x * _scaled_cell_width, y * _scaled_cell_height,
for col in range(buffer.rows[row].size()): (cols - x) * _scaled_cell_width, 1 * _scaled_cell_height), Color())
var data = buffer.rows[row][col] var color = _color_manager.colors.ansi[_work_cell.get_fg_color()] if _work_cell.get_fg_color() >= 0 else Color(1, 1, 1)
#print("row: ", ((row + 1) * charHeight), " col: ", (col * charWidth)) draw_char(options_service.options.font_family.regular,
_draw_character(col, row, data) Vector2(x * _scaled_cell_width + _scaled_char_left,
y * _scaled_cell_height + _scaled_char_top + _scaled_char_height / 2),
# Draw the cursor. _work_cell.get_chars() if _work_cell.get_chars() else ' ', "", color)
_draw_cursor() # Draw the cursor
# Draw selection
func _draw_character(col, row, data):
# Draw the background.
draw_rect(Rect2(Vector2(col * cell.x, row * cell.y), Vector2(cell.x, cell.y)), data.bg)
var font
if data.ff & (1 << Const.FONT_BOLD) and data.ff & (1 << Const.FONT_ITALIC):
font = bold_italic_font
elif data.ff & (1 << Const.FONT_BOLD):
font = bold_font
elif data.ff & (1 << Const.FONT_ITALIC):
font = italic_font
else:
font = normal_font
# Draw the character using foreground color.
draw_char(font, Vector2(col * cell.x, (row + 1) * cell.y), data.ch, '', data.fg)
func _draw_cursor():
draw_rect(Rect2(Vector2(buffer.ccol * cell.x, buffer.crow * cell.y), Vector2(cell.x, cell.y)), Color(1.0, 0.0, 1.0))
func receive_data(data: PoolByteArray):
var utf32 = []
var length = decoder.decode(data, utf32)
parser.parse(utf32, length)
update()

View file

@ -8,11 +8,28 @@
config_version=4 config_version=4
_global_script_classes=[ ] _global_script_classes=[ {
"base": "Node2D",
"class": "CanvasRenderingContext2D",
"language": "GDScript",
"path": "res://addons/godot_xterm/renderer/canvas_rendering_context_2d.gd"
} ]
_global_script_class_icons={ _global_script_class_icons={
"CanvasRenderingContext2D": ""
} }
[WAT]
Test_Directory="res://tests"
Results_Directory="res://tests/results/WAT"
Minimize_Window_When_Running_Tests=false
TestStrategy={
"repeat": 1,
"strategy": "RunAll"
}
Tags=PoolStringArray( )
Display=8
[application] [application]
config/name="GodotXterm" config/name="GodotXterm"

View file

@ -5,15 +5,20 @@ extends Control
signal data_received(data) signal data_received(data)
# The user must have these programs installed for this to work. # The user must have these programs installed for this to work.
const dependencies = PoolStringArray(['which', 'socat', 'bash']) const dependencies = PoolStringArray(['which', 'socat', 'bash'])
const host = '127.0.0.1' const host = '127.0.0.1'
const port = 17154 const port = 7154
# Enable recording of all data send to the psuedoterminal master.
# This is useful if you want to record a session if you are trying
# to make a showcase of the terminal ;-)
export var record: bool = false
export(String) var record_file_path = '/tmp/godot-xterm-record.json'
var socat_pid = -1 var socat_pid = -1
var stream_peer = StreamPeerTCP.new() var stream_peer = StreamPeerTCP.new()
var record_file
func _ready(): func _ready():
@ -22,11 +27,11 @@ func _ready():
if exit_code != 0: if exit_code != 0:
OS.alert("Make sure the following programs are installed and in your $PATH: " + \ OS.alert("Make sure the following programs are installed and in your $PATH: " + \
dependencies.join(", ") + ".", "Misssing Dependencies!") dependencies.join(", ") + ".", "Misssing Dependencies!")
else:
# Start socat. # Start socat.
socat_pid = OS.execute("socat", socat_pid = OS.execute("socat",
["-d", "-d", "tcp-l:%d,bind=%s,reuseaddr,fork" % [port, host], ["-d", "-d", "tcp-l:%d,bind=%s,reuseaddr,fork" % [port, host],
"exec:bash,pty,setsid,stderr,login,ctty"], false) "exec:bash,pty,setsid,stderr,login,ctty"], false)
# Create a StreamPeerTCP to connect to socat. # Create a StreamPeerTCP to connect to socat.
var err = stream_peer.connect_to_host(host, port) var err = stream_peer.connect_to_host(host, port)
@ -37,12 +42,21 @@ func _ready():
var status = stream_peer.get_status() var status = stream_peer.get_status()
var connected = stream_peer.is_connected_to_host() var connected = stream_peer.is_connected_to_host()
# Set the TERM environment variable, so that the correct escape sequences
# are sent to Terminal. By default this is set to dumb, which lacks support
# for even simple commands such as clear and reset.
stream_peer.put_data("export TERM=xterm\n".to_ascii())
stream_peer.put_data("clear\n".to_ascii())
# Connect the Terminal and StreamPeer. # Connect the Terminal and StreamPeer.
$Terminal.connect('data_sent', self, 'send_data') $Terminal.connect('output', self, 'send_data')
connect("data_received", $Terminal, "receive_data") connect("data_received", $Terminal, "write")
func send_data(data: PoolByteArray): func send_data(data: PoolByteArray):
if record:
# Save the data and timestamp to a file
record_file.write()
stream_peer.put_data(data) stream_peer.put_data(data)
@ -59,5 +73,7 @@ func _process(delta):
func _exit_tree(): func _exit_tree():
if record:
record_file.close()
if socat_pid != -1: if socat_pid != -1:
OS.execute("kill", ["-9", socat_pid], false) OS.execute("kill", ["-9", socat_pid], false)

View file

@ -1,24 +1,40 @@
[gd_scene load_steps=3 format=2] [gd_scene load_steps=7 format=2]
[ext_resource path="res://scenes/demo.gd" type="Script" id=1] [ext_resource path="res://scenes/demo.gd" type="Script" id=1]
[ext_resource path="res://addons/godot_xterm/terminal.gd" type="Script" id=2] [ext_resource path="res://addons/godot_xterm/terminal.gd" type="Script" id=2]
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres" type="DynamicFont" id=3]
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_italic.tres" type="DynamicFont" id=4]
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold_italic.tres" type="DynamicFont" id=5]
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres" type="DynamicFont" id=6]
[node name="Demo" type="Control"] [node name="Demo" type="Control"]
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
margin_left = 120.0
margin_top = 80.0
margin_right = 120.0
margin_bottom = 80.0
script = ExtResource( 1 ) script = ExtResource( 1 )
__meta__ = { __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": false
} }
[node name="Terminal" type="Control" parent="."] [node name="Terminal" type="Control" parent="."]
margin_right = 631.0 margin_left = 95.937
margin_bottom = 401.0 margin_top = 44.6138
margin_right = 695.937
margin_bottom = 444.614
rect_min_size = Vector2( 600, 400 )
script = ExtResource( 2 ) script = ExtResource( 2 )
__meta__ = { __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": false
} }
font_family = {
"bold": ExtResource( 6 ),
"bold_italic": ExtResource( 5 ),
"italic": ExtResource( 4 ),
"regular": ExtResource( 3 )
}
font_size = 16
colors = {
"black": Color( 0.121569, 0.00784314, 0.00784314, 1 )
}
window_options = {
}

39
scenes/showcase.tscn Normal file
View file

@ -0,0 +1,39 @@
[gd_scene load_steps=7 format=2]
[ext_resource path="res://scenes/demo.gd" type="Script" id=1]
[ext_resource path="res://addons/godot_xterm/terminal.gd" type="Script" id=2]
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres" type="DynamicFont" id=3]
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_italic.tres" type="DynamicFont" id=4]
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold_italic.tres" type="DynamicFont" id=5]
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres" type="DynamicFont" id=6]
[node name="Demo" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Terminal" type="Control" parent="."]
margin_left = 163.651
margin_top = 68.7974
margin_right = 763.651
margin_bottom = 468.797
rect_min_size = Vector2( 600, 400 )
script = ExtResource( 2 )
__meta__ = {
"_edit_use_anchors_": false
}
font_family = {
"bold": ExtResource( 6 ),
"bold_italic": ExtResource( 5 ),
"italic": ExtResource( 4 ),
"regular": ExtResource( 3 )
}
colors = {
"black": Color( 0.121569, 0.00784314, 0.00784314, 1 )
}
window_options = {
}

View file

@ -25,7 +25,7 @@ class TestBuffer:
func handle_csi(params): func handle_csi(params):
calls.append(['csi', params]) calls.append(['csi', params.to_array()])
func clear(): func clear():
@ -68,7 +68,7 @@ func test_prints_printables():
func skip_test_c0(): func skip_test_c0():
for code in C0.values(): for code in C0.values():
parser.set_execute_handler(code, buffer, 'handle_exec') parser.set_execute_handler(code, buffer, 'handle_exec')
parse(parser, Decoder.string_from_codepoint(code)) parse(parser, char(code))
if code == 0x0 or code == 0x1b or code == 0x20 or code == 0x7f: if code == 0x0 or code == 0x1b or code == 0x20 or code == 0x7f:
assert_eq(buffer.calls, []) assert_eq(buffer.calls, [])
else: else:
@ -81,7 +81,7 @@ func skip_test_c0():
func skip_test_c1(): func skip_test_c1():
for code in C1.values(): for code in C1.values():
parser.set_execute_handler(code, buffer, 'handle_exec') parser.set_execute_handler(code, buffer, 'handle_exec')
parse(parser, Decoder.string_from_codepoint(code)) parse(parser, char(code))
assert_eq(buffer.calls, [['exec']], 'code: 0x%x' % code) assert_eq(buffer.calls, [['exec']], 'code: 0x%x' % code)
assert_eq(buffer.printed, '') assert_eq(buffer.printed, '')
parser.reset() parser.reset()

View file

@ -12,6 +12,6 @@ __meta__ = {
"_edit_use_anchors_": false "_edit_use_anchors_": false
} }
_yield_between_tests = false _yield_between_tests = false
_directory1 = "res://test/unit" _include_subdirectories = true
_directory2 = "res://test/integration" _directory1 = "res://test"
_double_strategy = 1 _double_strategy = 1

66
test/test_utils.gd Normal file
View file

@ -0,0 +1,66 @@
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends "res://addons/gut/test.gd"
const Buffer = preload("res://addons/godot_xterm/buffer/buffer.gd")
const BufferSet = preload("res://addons/godot_xterm/buffer/buffer_set.gd")
const OptionsService = preload("res://addons/godot_xterm/services/options_service.gd")
class MockBufferService:
extends Reference
signal resized(cols, rows)
var service_brand
var buffer setget ,_get_buffer
var buffers
var is_user_scrolling: bool = false
var cols
var rows
func _get_buffer():
return buffers.active
func _init(cols: int, rows: int, options_service = MockOptionsService.new()):
self.cols = cols
self.rows = rows
buffers = BufferSet.new(options_service, self)
func resize(cols: int, rows: int) -> void:
self.cols = cols
self.rows = rows
func reset() -> void:
pass
class MockOptionsService:
extends Reference
signal option_changed
var service_brand
var options = OptionsService.TerminalOptions.new()
func _init(test_options = null):
if test_options:
for key in test_options.keys():
self.options.set(key, test_options[key])
func set_option(key: String, value) -> void:
pass
func get_option(key: String):
pass

Binary file not shown.

View file

@ -0,0 +1,128 @@
# Copyright (c) 2017 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends "res://addons/gut/test.gd"
const Buffer = preload("res://addons/godot_xterm/buffer/buffer.gd")
const CircularList = preload("res://addons/godot_xterm/circular_list.gd")
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
const TestUtils = preload("res://test/test_utils.gd")
class BaseBufferTest:
extends "res://addons/gut/test.gd"
const INIT_COLS = 80
const INIT_ROWS = 24
const INIT_SCROLLBACK = 1000
var options_service
var buffer_service
var buffer
func before_each():
options_service = TestUtils.MockOptionsService.new({'scrollback': INIT_SCROLLBACK})
buffer_service = TestUtils.MockBufferService.new(INIT_COLS, INIT_ROWS)
buffer = Buffer.new(true, options_service, buffer_service)
class TestInit:
extends BaseBufferTest
func test_buffer_lines():
assert_eq(buffer.lines.get_script(), CircularList)
assert_eq(buffer.lines.max_length, buffer_service.rows + INIT_SCROLLBACK)
func test_buffer_scroll_bottom():
assert_eq(buffer.scroll_bottom, buffer_service.rows - 1)
func test_fill_viewport_rows():
# It should fill the buffer with blank lines based on the size of the viewport
var blank_line_char = buffer.get_blank_line(AttributeData.new()).load_cell(0, CellData.new()).get_as_char_data()
buffer.fill_viewport_rows()
assert_eq(buffer.lines.length, INIT_ROWS)
for y in range(INIT_ROWS):
assert_eq(buffer.lines.get_el(y).length, INIT_COLS)
for x in range(INIT_COLS):
assert_eq(buffer.lines.get_el(y).load_cell(x, CellData.new()).get_as_char_data(), blank_line_char)
class TestGetWrappedRangeForLine:
extends BaseBufferTest
func before_each():
.before_each()
buffer.fill_viewport_rows()
func test_non_wrapped_returns_a_single_row_for_the_first_row():
assert_eq(buffer.get_wrapped_range_for_line(0).first, 0)
assert_eq(buffer.get_wrapped_range_for_line(0).last, 0)
func test_non_wrapped_returns_a_single_row_for_a_middle_row():
assert_eq(buffer.get_wrapped_range_for_line(12).first, 12)
assert_eq(buffer.get_wrapped_range_for_line(12).last, 12)
func test_non_wrapped_returns_a_single_row_for_the_last_row():
assert_eq(buffer.get_wrapped_range_for_line(buffer.lines.length - 1).first, INIT_ROWS - 1)
assert_eq(buffer.get_wrapped_range_for_line(buffer.lines.length - 1).last, INIT_ROWS - 1)
func test_wrapped_returns_a_range_for_first_row():
buffer.lines.get_el(1).is_wrapped = true
assert_eq(buffer.get_wrapped_range_for_line(0).first, 0)
assert_eq(buffer.get_wrapped_range_for_line(0).last, 1)
func test_wrapped_range_for_middle_row_wrapping_upwards():
buffer.fill_viewport_rows()
buffer.lines.get_el(12).is_wrapped = true
assert_eq(buffer.get_wrapped_range_for_line(12).first, 11)
assert_eq(buffer.get_wrapped_range_for_line(12).last, 12)
func test_wrapped_range_for_middle_row_wrapping_downwards():
buffer.lines.get_el(13).is_wrapped = true
assert_eq(buffer.get_wrapped_range_for_line(12).first, 12)
assert_eq(buffer.get_wrapped_range_for_line(12).last, 13)
func test_wrapped_range_for_middle_row_wrapping_both_ways():
buffer.lines.get_el(11).is_wrapped = true
buffer.lines.get_el(12).is_wrapped = true
buffer.lines.get_el(13).is_wrapped = true
buffer.lines.get_el(14).is_wrapped = true
assert_eq(buffer.get_wrapped_range_for_line(12).first, 10)
assert_eq(buffer.get_wrapped_range_for_line(12).last, 14)
func test_wrapped_range_for_last_row():
buffer.lines.get_el(INIT_ROWS - 1).is_wrapped = true
assert_eq(buffer.get_wrapped_range_for_line(buffer.lines.length - 1).first, INIT_ROWS - 2)
assert_eq(buffer.get_wrapped_range_for_line(buffer.lines.length - 1).last, INIT_ROWS - 1)
func test_wrapped_range_for_row_that_wraps_upward_to_first_row():
buffer.lines.get_el(1).is_wrapped = true
assert_eq(buffer.get_wrapped_range_for_line(1).first, 0)
assert_eq(buffer.get_wrapped_range_for_line(1).last, 1)
func test_wrapped_range_for_row_that_wraps_downward_to_last_row():
buffer.lines.get_el(buffer.lines.length - 1).is_wrapped = true
assert_eq(buffer.get_wrapped_range_for_line(buffer.lines.length - 2).first, INIT_ROWS - 2)
assert_eq(buffer.get_wrapped_range_for_line(buffer.lines.length - 2).last, INIT_ROWS - 1)

View file

@ -0,0 +1,348 @@
# Copyright (c) 2018 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends "res://addons/gut/test.gd"
const BufferLine = preload("res://addons/godot_xterm/buffer/buffer_line.gd")
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
const BgFlags = Constants.BgFlags
const Attributes = Constants.Attributes
const UnderlineStyle = Constants.UnderlineStyle
const FgFlags = Constants.FgFlags
const Content = Constants.Content
class BufferLineTest:
extends BufferLine
func _init(cols: int, fill_cell_data = null, is_wrapped: bool = false).(cols, fill_cell_data, is_wrapped):
pass
func to_array():
var result = []
for i in range(length):
result.append(load_cell(i, CellData.new()).get_as_char_data())
return result
class TestAttributeData:
extends "res://addons/gut/test.gd"
var attrs
func before_each():
attrs = AttributeData.new()
func test_has_extended_attrs():
assert_eq(attrs.has_extended_attrs() as bool, false)
attrs.bg |= BgFlags.HAS_EXTENDED
assert_eq(attrs.has_extended_attrs() as bool, true)
func test_get_underline_color_P256():
# set a P256 color
attrs.extended.underline_color = Attributes.CM_P256 | 45
# should use FG color if BgFlags.HAS_EXTENDED is not set
assert_eq(attrs.get_underline_color(), -1)
# should use underline_color is BgFlags.HAS_EXTENDED is set and underline_color holds a value
attrs.bg |= BgFlags.HAS_EXTENDED
assert_eq(attrs.get_underline_color(), 45)
# should use FG color if underline_color holds no value
attrs.extended.underline_color = -1
attrs.fg |= Attributes.CM_P256 | 123
assert_eq(attrs.get_underline_color(), 123)
func test_get_underline_color_RGB():
# set a P256 color
attrs.extended.underline_color = Attributes.CM_RGB | (1 << 16) | (2 << 8) | 3
# should use FG color if BgFlags.HAS_EXTENDED is not set
assert_eq(attrs.get_underline_color(), -1)
# should use underline_color if BgFlags.HAS_EXTENDED is set and underline_color holds a value
attrs.bg |= BgFlags.HAS_EXTENDED
assert_eq(attrs.get_underline_color(), (1 << 16) | (2 << 8) | 3)
# should use FG color if underline_color holds no value
attrs.extended.underline_color = -1
attrs.fg |= Attributes.CM_P256 | 123
assert_eq(attrs.get_underline_color(), 123)
func test_underline_attrs():
# should always return color mode of fg
for mode in [Attributes.CM_DEFAULT, Attributes.CM_P16, Attributes.CM_P256, Attributes.CM_RGB]:
attrs.extended.underline_color = mode
assert_eq(attrs.get_underline_color_mode(), attrs.get_fg_color_mode())
assert_eq(attrs.is_underline_color_default(), true)
attrs.fg = Attributes.CM_RGB
for mode in [Attributes.CM_DEFAULT, Attributes.CM_P16, Attributes.CM_P256, Attributes.CM_RGB]:
attrs.extended.underline_color = mode
assert_eq(attrs.get_underline_color_mode(), attrs.get_fg_color_mode())
assert_eq(attrs.is_underline_color_default(), false)
assert_eq(attrs.is_underline_color_rgb(), true)
# should return own mode
attrs.bg |= BgFlags.HAS_EXTENDED
attrs.extended.underline_color = Attributes.CM_DEFAULT
assert_eq(attrs.get_underline_color_mode(), Attributes.CM_DEFAULT)
attrs.extended.underline_color = Attributes.CM_P16
assert_eq(attrs.get_underline_color_mode(), Attributes.CM_P16)
assert_eq(attrs.is_underline_color_palette(), true)
attrs.extended.underline_color = Attributes.CM_P256
assert_eq(attrs.get_underline_color_mode(), Attributes.CM_P256)
assert_eq(attrs.is_underline_color_palette(), true)
attrs.extended.underline_color = Attributes.CM_RGB
assert_eq(attrs.get_underline_color_mode(), Attributes.CM_RGB)
assert_eq(attrs.is_underline_color_rgb(), true)
func test_get_underline_style():
# defaults to no underline style
assert_eq(attrs.get_underline_style(), UnderlineStyle.NONE)
# should return NONE if UNDERLINE is not set
attrs.extended.underline_style = UnderlineStyle.CURLY
assert_eq(attrs.get_underline_style(), UnderlineStyle.NONE)
# should return SINGLE style if UNDERLINE is set and HAS_EXTENDED is false
attrs.fg |= FgFlags.UNDERLINE
assert_eq(attrs.get_underline_style(), UnderlineStyle.SINGLE)
# shoud return correct style if both is set
attrs.bg |= BgFlags.HAS_EXTENDED
assert_eq(attrs.get_underline_style(), UnderlineStyle.CURLY)
# should return NONE if UNDERLINE is not set, but HAS_EXTENDED is true
attrs.fg &= ~FgFlags.UNDERLINE
assert_eq(attrs.get_underline_style(), UnderlineStyle.NONE)
class TestCellData:
extends "res://addons/gut/test.gd"
var cell
var decoder = Decoder.Utf8ToUtf32.new()
func before_each():
cell = CellData.new()
func test_char_data_cell_data_equality():
# ASCII
cell.set_from_char_data([123, 'a', 1, 'a'.ord_at(0)])
assert_eq(cell.get_as_char_data(), [123, 'a', 1, 'a'.ord_at(0)])
assert_eq(cell.is_combined(), 0)
# combining
cell.set_from_char_data([123, 'e\u0301', 1, '\u0301'.ord_at(0)])
assert_eq(cell.get_as_char_data(), [123, 'e\u0301', 1, '\u0301'.ord_at(0)])
assert_eq(cell.is_combined(), Content.IS_COMBINED_MASK)
# surrogate
cell.set_from_char_data([123, '𝄞', 1, 0x1D11E])
assert_eq(cell.get_as_char_data(), [123, '𝄞', 1, 0x1D11E])
assert_eq(cell.is_combined(), 0)
# surrogate + combining
cell.set_from_char_data([123, '𓂀\u0301', 1, '𓂀\u0301'.ord_at(1)])
assert_eq(cell.get_as_char_data(), [123, '𓂀\u0301', 1, '𓂀\u0301'.ord_at(1)])
assert_eq(cell.is_combined(), Content.IS_COMBINED_MASK)
# wide char
cell.set_from_char_data([123, '', 2, ''.ord_at(0)])
assert_eq(cell.get_as_char_data(), [123, '', 2, ''.ord_at(0)])
assert_eq(cell.is_combined(), 0)
class TestBufferLine:
extends "res://addons/gut/test.gd"
func test_ctor():
var line = BufferLineTest.new(0)
assert_eq(line.length, 0)
assert_eq(line.is_wrapped, false)
line = BufferLineTest.new(10)
assert_eq(line.length, 10)
assert_eq(line.load_cell(0, CellData.new()).get_as_char_data(),
[0, Constants.NULL_CELL_CHAR, Constants.NULL_CELL_WIDTH,
Constants.NULL_CELL_CODE])
assert_eq(line.is_wrapped, false)
line = BufferLineTest.new(10, null, true)
assert_eq(line.length, 10)
assert_eq(line.load_cell(0, CellData.new()).get_as_char_data(),
[0, Constants.NULL_CELL_CHAR, Constants.NULL_CELL_WIDTH,
Constants.NULL_CELL_CODE])
assert_eq(line.is_wrapped, true)
var char_data = [123, 'a', 456, 'a'.ord_at(0)]
line = BufferLineTest.new(10, CellData.from_char_data(char_data), true)
assert_eq(line.length, 10)
assert_eq(line.load_cell(0, CellData.new()).get_as_char_data(), char_data)
assert_eq(line.is_wrapped, true)
func test_insert_cells() -> void:
var line = BufferLineTest.new(3)
line.set_cell(0, CellData.from_char_data([1, 'a', 0, 'a'.ord_at(0)]))
line.set_cell(1, CellData.from_char_data([2, 'b', 0, 'b'.ord_at(0)]))
line.set_cell(2, CellData.from_char_data([3, 'c', 0, 'c'.ord_at(0)]))
line.insert_cells(1, 3, CellData.from_char_data([4, 'd', 0, 'd'.ord_at(0)]))
assert_eq(line.to_array(), [
[1, 'a', 0, 'a'.ord_at(0)],
[4, 'd', 0, 'd'.ord_at(0)],
[4, 'd', 0, 'd'.ord_at(0)]
])
func test_delete_cells() -> void:
var line = BufferLineTest.new(5)
line.set_cell(0, CellData.from_char_data([1, 'a', 0, 'a'.ord_at(0)]))
line.set_cell(1, CellData.from_char_data([2, 'b', 0, 'b'.ord_at(0)]))
line.set_cell(2, CellData.from_char_data([3, 'c', 0, 'c'.ord_at(0)]))
line.set_cell(3, CellData.from_char_data([4, 'd', 0, 'd'.ord_at(0)]))
line.set_cell(4, CellData.from_char_data([5, 'e', 0, 'e'.ord_at(0)]))
line.delete_cells(1, 2, CellData.from_char_data([6, 'f', 0, 'f'.ord_at(0)]))
assert_eq(line.to_array(), [
[1, 'a', 0, 'a'.ord_at(0)],
[4, 'd', 0, 'd'.ord_at(0)],
[5, 'e', 0, 'e'.ord_at(0)],
[6, 'f', 0, 'f'.ord_at(0)],
[6, 'f', 0, 'f'.ord_at(0)]
])
func test_replace_cells():
var line = BufferLineTest.new(5)
line.set_cell(0, CellData.from_char_data([1, 'a', 0, 'a'.ord_at(0)]))
line.set_cell(1, CellData.from_char_data([2, 'b', 0, 'b'.ord_at(0)]))
line.set_cell(2, CellData.from_char_data([3, 'c', 0, 'c'.ord_at(0)]))
line.set_cell(3, CellData.from_char_data([4, 'd', 0, 'd'.ord_at(0)]))
line.set_cell(4, CellData.from_char_data([5, 'e', 0, 'e'.ord_at(0)]))
line.replace_cells(2, 4, CellData.from_char_data([6, 'f', 0, 'f'.ord_at(0)]))
assert_eq(line.to_array(), [
[1, 'a', 0, 'a'.ord_at(0)],
[2, 'b', 0, 'b'.ord_at(0)],
[6, 'f', 0, 'f'.ord_at(0)],
[6, 'f', 0, 'f'.ord_at(0)],
[5, 'e', 0, 'e'.ord_at(0)],
])
# Skipped a bunch of tests here...
class TestAddCharToCell:
extends "res://addons/gut/test.gd"
var line
var cell
func before_each():
line = BufferLineTest.new(3, CellData.from_char_data([Constants.DEFAULT_ATTR,
Constants.NULL_CELL_CHAR, Constants.NULL_CELL_WIDTH, Constants.NULL_CELL_CODE]))
cell = line.load_cell(0, CellData.new())
func test_sets_width_to_1_for_empty_cell():
line.add_codepoint_to_cell(0, "\u0301".ord_at(0))
cell = line.load_cell(0, CellData.new())
# chars contains single combining char
# width is set to 1
assert_eq(cell.get_as_char_data(), [Constants.DEFAULT_ATTR, '\u0301', 1, 0x0301])
# do not account a single combining char as combined
assert_eq(cell.is_combined(), 0)
func test_add_char_to_combining_string_in_cell():
cell.set_from_char_data([123, "e\u0301", 1, "e\u0301".ord_at(1)])
line.set_cell(0, cell)
line.add_codepoint_to_cell(0, "\u0301".ord_at(0))
line.load_cell(0, cell)
# char contains 3 chars
# width is set to 1
assert_eq(cell.get_as_char_data(), [123, "e\u0301\u0301", 1, 0x0301])
# do not account a single combining char as combined
assert_eq(cell.is_combined(), Content.IS_COMBINED_MASK)
func test_create_combining_string_on_taken_cell():
cell.set_from_char_data([123, "e", 1, "e".ord_at(1)])
line.set_cell(0, cell)
line.add_codepoint_to_cell(0, "\u0301".ord_at(0))
line.load_cell(0, cell)
# chars contains 2 chars
# width is set to 1
assert_eq(cell.get_as_char_data(), [123, "e\u0301", 1, 0x0301])
# do not account a single combining char as combined
assert_eq(cell.is_combined(), Content.IS_COMBINED_MASK)
class Testtranslate_to_string:
extends "res://addons/gut/test.gd"
var line
func before_each():
line = BufferLineTest.new(10, CellData.from_char_data([Constants.DEFAULT_ATTR,
Constants.NULL_CELL_CHAR, Constants.NULL_CELL_WIDTH, Constants.NULL_CELL_CODE]), false)
func test_empty_line():
assert_eq(line.translate_to_string(false), ' ')
assert_eq(line.translate_to_string(true), '')
func test_ASCII():
line.set_cell(0, CellData.from_char_data([1, 'a', 1, 'a'.ord_at(0)]))
line.set_cell(2, CellData.from_char_data([1, 'a', 1, 'a'.ord_at(0)]))
line.set_cell(4, CellData.from_char_data([1, 'a', 1, 'a'.ord_at(0)]))
line.set_cell(5, CellData.from_char_data([1, 'a', 1, 'a'.ord_at(0)]))
assert_eq(line.translate_to_string(false), 'a a aa ')
assert_eq(line.translate_to_string(true), 'a a aa')
assert_eq(line.translate_to_string(false, 0, 5), 'a a a')
assert_eq(line.translate_to_string(false, 0, 4), 'a a ')
assert_eq(line.translate_to_string(false, 0, 3), 'a a')
assert_eq(line.translate_to_string(true, 0, 5), 'a a a')
assert_eq(line.translate_to_string(true, 0, 4), 'a a ')
assert_eq(line.translate_to_string(true, 0, 3), 'a a')
func test_space_at_end():
line.set_cell(0, CellData.from_char_data([1, 'a', 1, 'a'.ord_at(0)]))
line.set_cell(2, CellData.from_char_data([1, 'a', 1, 'a'.ord_at(0)]))
line.set_cell(4, CellData.from_char_data([1, 'a', 1, 'a'.ord_at(0)]))
line.set_cell(5, CellData.from_char_data([1, 'a', 1, 'a'.ord_at(0)]))
line.set_cell(6, CellData.from_char_data([1, ' ', 1, ' '.ord_at(0)]))
assert_eq(line.translate_to_string(false), 'a a aa ')
assert_eq(line.translate_to_string(true), 'a a aa ')
func test_always_returns_some_sane_value():
# sanity check - broken line with invalid out of bound null width cells
# this can atm happen with deleting/inserting chars in inputhandler by "breaking"
# fullwidth pairs --> needs to be fixed after settling BufferLine impl
assert_eq(line.translate_to_string(false), ' ')
assert_eq(line.translate_to_string(true), '')
func test_works_with_end_col_0():
line.set_cell(0, CellData.from_char_data([1, 'a', 1, 'a'.ord_at(0)]))
assert_eq(line.translate_to_string(true, 0, 0), '')

View file

@ -16,9 +16,10 @@ const TEST_STRINGS = [
"모든 국민은 행위시의 법률에 의하여 범죄를 구성하지 아니하는 행위로 소추되지 아니하며. 전직대통령의 신분과 예우에 관하여는 법률로 정한다, 국회는 헌법 또는 법률에 특별한 규정이 없는 한 재적의원 과반수의 출석과 출석의원 과반수의 찬성으로 의결한다. 군인·군무원·경찰공무원 기타 법률이 정하는 자가 전투·훈련등 직무집행과 관련하여 받은 손해에 대하여는 법률이 정하는 보상외에 국가 또는 공공단체에 공무원의 직무상 불법행위로 인한 배상은 청구할 수 없다.", "모든 국민은 행위시의 법률에 의하여 범죄를 구성하지 아니하는 행위로 소추되지 아니하며. 전직대통령의 신분과 예우에 관하여는 법률로 정한다, 국회는 헌법 또는 법률에 특별한 규정이 없는 한 재적의원 과반수의 출석과 출석의원 과반수의 찬성으로 의결한다. 군인·군무원·경찰공무원 기타 법률이 정하는 자가 전투·훈련등 직무집행과 관련하여 받은 손해에 대하여는 법률이 정하는 보상외에 국가 또는 공공단체에 공무원의 직무상 불법행위로 인한 배상은 청구할 수 없다.",
"كان فشكّل الشرقي مع, واحدة للمجهود تزامناً بعض بل. وتم جنوب للصين غينيا لم, ان وبدون وكسبت الأمور ذلك, أسر الخاسر الانجليزية هو. نفس لغزو مواقعها هو. الجو علاقة الصعداء انه أي, كما مع بمباركة للإتحاد الوزراء. ترتيب الأولى أن حدى, الشتوية باستحداث مدن بل, كان قد أوسع عملية. الأوضاع بالمطالبة كل قام, دون إذ شمال الربيع،. هُزم الخاصّة ٣٠ أما, مايو الصينية مع قبل.", "كان فشكّل الشرقي مع, واحدة للمجهود تزامناً بعض بل. وتم جنوب للصين غينيا لم, ان وبدون وكسبت الأمور ذلك, أسر الخاسر الانجليزية هو. نفس لغزو مواقعها هو. الجو علاقة الصعداء انه أي, كما مع بمباركة للإتحاد الوزراء. ترتيب الأولى أن حدى, الشتوية باستحداث مدن بل, كان قد أوسع عملية. الأوضاع بالمطالبة كل قام, دون إذ شمال الربيع،. هُزم الخاصّة ٣٠ أما, مايو الصينية مع قبل.",
"או סדר החול מיזמי קרימינולוגיה. קהילה בגרסה לויקיפדים אל היא, של צעד ציור ואלקטרוניקה. מדע מה ברית המזנון ארכיאולוגיה, אל טבלאות מבוקשים כלל. מאמרשיחהצפה העריכהגירסאות שכל אל, כתב עיצוב מושגי של. קבלו קלאסיים ב מתן. נבחרים אווירונאוטיקה אם מלא, לוח למנוע ארכיאולוגיה מה. ארץ לערוך בקרבת מונחונים או, עזרה רקטות לויקיפדים אחר גם.", "או סדר החול מיזמי קרימינולוגיה. קהילה בגרסה לויקיפדים אל היא, של צעד ציור ואלקטרוניקה. מדע מה ברית המזנון ארכיאולוגיה, אל טבלאות מבוקשים כלל. מאמרשיחהצפה העריכהגירסאות שכל אל, כתב עיצוב מושגי של. קבלו קלאסיים ב מתן. נבחרים אווירונאוטיקה אם מלא, לוח למנוע ארכיאולוגיה מה. ארץ לערוך בקרבת מונחונים או, עזרה רקטות לויקיפדים אחר גם.",
"Лорем ლორემ अधिकांश 覧六子 八メル 모든 בקרבת 💮 😂 äggg 123€ 𝄞.", "Лорем ლორემ अधिकांश 覧六子 八メル 모든 בקרבת äggg 123€ .",
] ]
func test_utf32_to_utf8(): func test_utf32_to_utf8():
# 1 byte utf8 character # 1 byte utf8 character
assert_eq( assert_eq(
@ -45,10 +46,6 @@ func test_utf32_to_utf8():
PoolByteArray([0xf0, 0x9f, 0x90, 0xa7]) as Array PoolByteArray([0xf0, 0x9f, 0x90, 0xa7]) as Array
) )
func test_string_from_codepoint():
assert_eq(Decoder.string_from_codepoint(49), '1')
assert_eq(Decoder.string_from_codepoint(0x1f427), '🐧')
assert_eq(Decoder.string_from_codepoint(0x1d11e), '𝄞')
func test_utf32_to_string(): func test_utf32_to_string():
assert_eq( assert_eq(
@ -72,6 +69,11 @@ class TestUtf8ToUtf32Decoder:
target.clear() target.clear()
target.resize(5) target.resize(5)
func test_lol():
var target = [0, 0, 0, 0]
decoder.decode('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>'.substr(0, 1).to_utf8(), target)
assert_eq(target, [])
func test_full_code_point_0_to_65535(): # 1/2/3 byte sequences func test_full_code_point_0_to_65535(): # 1/2/3 byte sequences
if skip_full_codepoint_tests: if skip_full_codepoint_tests:

View file

@ -36,7 +36,7 @@ class TestTerminal:
func handle_execute(code: int): func handle_execute(code: int):
var flag = Decoder.string_from_codepoint(code) var flag = char(code)
calls.append(['exe', flag]) calls.append(['exe', flag])
@ -137,14 +137,14 @@ func test_state_GROUND_execute_action():
var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20) var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20)
for exe in exes: for exe in exes:
parser.current_state = ParserState.GROUND parser.current_state = ParserState.GROUND
parse(parser, Decoder.string_from_codepoint(exe)) parse(parser, char(exe))
assert_eq(parser.current_state, ParserState.GROUND) assert_eq(parser.current_state, ParserState.GROUND)
parser.reset() parser.reset()
func test_state_GROUND_print_action(): func test_state_GROUND_print_action():
var printables = range(0x20, 0x7f) # NOTE: DEL excluded var printables = range(0x20, 0x7f) # NOTE: DEL excluded
for printable in printables: for printable in printables:
var string = Decoder.string_from_codepoint(printable) var string = char(printable)
parser.current_state = ParserState.GROUND parser.current_state = ParserState.GROUND
parse(parser, string) parse(parser, string)
assert_eq(parser.current_state, ParserState.GROUND) assert_eq(parser.current_state, ParserState.GROUND)
@ -204,7 +204,7 @@ func test_state_ESCAPE_execute_rules():
var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20) var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20)
for exe in exes: for exe in exes:
parser.current_state = ParserState.ESCAPE parser.current_state = ParserState.ESCAPE
var data = Decoder.string_from_codepoint(exe) var data = char(exe)
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.ESCAPE, 'exe: %x' % exe) assert_eq(parser.current_state, ParserState.ESCAPE, 'exe: %x' % exe)
assert_eq(test_terminal.calls, [['exe', data]], 'exe: %x' % exe) assert_eq(test_terminal.calls, [['exe', data]], 'exe: %x' % exe)
@ -222,7 +222,7 @@ func test_trans_ESCAPE_to_GROUND_with_esc_dispatch_action():
var dispatches = range(0x30, 0x50) + range(0x51, 0x58) + [0x59, 0x5a] + range(0x60, 0x7f) var dispatches = range(0x30, 0x50) + range(0x51, 0x58) + [0x59, 0x5a] + range(0x60, 0x7f)
for dispatch in dispatches: for dispatch in dispatches:
parser.current_state = ParserState.ESCAPE parser.current_state = ParserState.ESCAPE
var data = Decoder.string_from_codepoint(dispatch) var data = char(dispatch)
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.GROUND, assert_eq(parser.current_state, ParserState.GROUND,
'wrong state: %s, dispatch: %x' % [ParserState.keys()[parser.current_state], dispatch]) 'wrong state: %s, dispatch: %x' % [ParserState.keys()[parser.current_state], dispatch])
@ -236,7 +236,7 @@ func test_trans_ESCAPE_to_ESCAPE_INTERMEDIATE_with_collect_action():
var collect = range(0x20, 0x30) var collect = range(0x20, 0x30)
for c in collect: for c in collect:
parser.current_state = ParserState.ESCAPE parser.current_state = ParserState.ESCAPE
var data = Decoder.string_from_codepoint(c) var data = char(c)
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.ESCAPE_INTERMEDIATE) assert_eq(parser.current_state, ParserState.ESCAPE_INTERMEDIATE)
assert_eq(parser.collect, data) assert_eq(parser.collect, data)
@ -246,7 +246,7 @@ func test_trans_ESCAPE_to_ESCAPE_INTERMEDIATE_with_collect_action():
func test_state_ESCAPE_INTERMEDIATE_execute_rules(): func test_state_ESCAPE_INTERMEDIATE_execute_rules():
var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20) var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20)
for exe in exes: for exe in exes:
var data = Decoder.string_from_codepoint(exe) var data = char(exe)
parser.current_state = ParserState.ESCAPE_INTERMEDIATE parser.current_state = ParserState.ESCAPE_INTERMEDIATE
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.ESCAPE_INTERMEDIATE) assert_eq(parser.current_state, ParserState.ESCAPE_INTERMEDIATE)
@ -265,7 +265,7 @@ func test_state_ESCAPE_INTERMEDIATE_ignore():
func test_state_ESCAPE_INTERMEDIATE_collect_action(): func test_state_ESCAPE_INTERMEDIATE_collect_action():
var collect = range(0x20, 0x30) var collect = range(0x20, 0x30)
for c in collect: for c in collect:
var data = Decoder.string_from_codepoint(c) var data = char(c)
parser.current_state = ParserState.ESCAPE_INTERMEDIATE parser.current_state = ParserState.ESCAPE_INTERMEDIATE
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.ESCAPE_INTERMEDIATE) assert_eq(parser.current_state, ParserState.ESCAPE_INTERMEDIATE)
@ -276,7 +276,7 @@ func test_state_ESCAPE_INTERMEDIATE_collect_action():
func test_trans_ESCAPE_INTERMEDIATE_to_GROUND_with_esc_dispatch_action(): func test_trans_ESCAPE_INTERMEDIATE_to_GROUND_with_esc_dispatch_action():
var collect = range(0x30, 0x7f) var collect = range(0x30, 0x7f)
for c in collect: for c in collect:
var data = Decoder.string_from_codepoint(c) var data = char(c)
parser.current_state = ParserState.ESCAPE_INTERMEDIATE parser.current_state = ParserState.ESCAPE_INTERMEDIATE
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.GROUND) assert_eq(parser.current_state, ParserState.GROUND)
@ -309,7 +309,7 @@ func test_ANYWHERE_or_ESCAPE_to_CSI_ENTRY_with_clear():
func test_CSI_ENTRY_execute_rules(): func test_CSI_ENTRY_execute_rules():
var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20) var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20)
for exe in exes: for exe in exes:
var data = Decoder.string_from_codepoint(exe) var data = char(exe)
parser.current_state = ParserState.CSI_ENTRY parser.current_state = ParserState.CSI_ENTRY
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.CSI_ENTRY) assert_eq(parser.current_state, ParserState.CSI_ENTRY)
@ -328,7 +328,7 @@ func test_state_CSI_ENTRY_ignore():
func test_trans_CSI_ENTRY_to_GROUND_with_csi_dispatch_action(): func test_trans_CSI_ENTRY_to_GROUND_with_csi_dispatch_action():
var dispatches = range(0x40, 0x7f) var dispatches = range(0x40, 0x7f)
for dispatch in dispatches: for dispatch in dispatches:
var data = Decoder.string_from_codepoint(dispatch) var data = char(dispatch)
parser.current_state = ParserState.CSI_ENTRY parser.current_state = ParserState.CSI_ENTRY
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.GROUND) assert_eq(parser.current_state, ParserState.GROUND)
@ -342,7 +342,7 @@ func test_trans_CSI_ENTRY_to_CSI_PARAMS_with_param_or_collect_action():
var collect = ['\u003c', '\u003d', '\u003e', '\u003f'] var collect = ['\u003c', '\u003d', '\u003e', '\u003f']
for param in params: for param in params:
parser.current_state = ParserState.CSI_ENTRY parser.current_state = ParserState.CSI_ENTRY
parse(parser, Decoder.string_from_codepoint(param)) parse(parser, char(param))
assert_eq(parser.current_state, ParserState.CSI_PARAM) assert_eq(parser.current_state, ParserState.CSI_PARAM)
assert_eq(parser.params, [param - 48], 'param: 0x%x' % param) assert_eq(parser.params, [param - 48], 'param: 0x%x' % param)
parser.reset() parser.reset()
@ -362,7 +362,7 @@ func test_trans_CSI_ENTRY_to_CSI_PARAMS_with_param_or_collect_action():
func test_state_CSI_PARAM_execute_rules(): func test_state_CSI_PARAM_execute_rules():
var exes = range(0x00, 0x018) + [0x19] + range(0x1c, 0x20) var exes = range(0x00, 0x018) + [0x19] + range(0x1c, 0x20)
for exe in exes: for exe in exes:
var data = Decoder.string_from_codepoint(exe) var data = char(exe)
parser.current_state = ParserState.CSI_PARAM parser.current_state = ParserState.CSI_PARAM
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.CSI_PARAM) assert_eq(parser.current_state, ParserState.CSI_PARAM)
@ -375,7 +375,7 @@ func test_state_CSI_PARAM_param_action():
var params = range(0x30, 0x3a) var params = range(0x30, 0x3a)
for param in params: for param in params:
parser.current_state = ParserState.CSI_PARAM parser.current_state = ParserState.CSI_PARAM
parse(parser, Decoder.string_from_codepoint(param)) parse(parser, char(param))
assert_eq(parser.current_state, ParserState.CSI_PARAM) assert_eq(parser.current_state, ParserState.CSI_PARAM)
assert_eq(parser.params, [param - 48], 'param: 0x%x' % param) assert_eq(parser.params, [param - 48], 'param: 0x%x' % param)
parser.reset() parser.reset()
@ -391,7 +391,7 @@ func test_state_CSI_PARAM_ignore():
func test_trans_CSI_PARAM_to_GROUND_with_csi_dispatch_action(): func test_trans_CSI_PARAM_to_GROUND_with_csi_dispatch_action():
var dispatches = range(0x40, 0x7f) var dispatches = range(0x40, 0x7f)
for dispatch in dispatches: for dispatch in dispatches:
var data = Decoder.string_from_codepoint(dispatch) var data = char(dispatch)
parser.current_state = ParserState.CSI_PARAM parser.current_state = ParserState.CSI_PARAM
parser.params = [0, 1] parser.params = [0, 1]
parse(parser, data) parse(parser, data)
@ -403,7 +403,7 @@ func test_trans_CSI_PARAM_to_GROUND_with_csi_dispatch_action():
func test_trans_CSI_ENTRY_to_CSI_INTERMEDIATE_with_collect_action(): func test_trans_CSI_ENTRY_to_CSI_INTERMEDIATE_with_collect_action():
for collect in range(0x20, 0x30): for collect in range(0x20, 0x30):
var data = Decoder.string_from_codepoint(collect) var data = char(collect)
parser.current_state = ParserState.CSI_ENTRY parser.current_state = ParserState.CSI_ENTRY
parse(parser, data) parse(parser, data)
assert_eq(parser.current_state, ParserState.CSI_INTERMEDIATE) assert_eq(parser.current_state, ParserState.CSI_INTERMEDIATE)

View file

@ -0,0 +1,202 @@
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends 'res://addons/gut/test.gd'
const Params = preload("res://addons/godot_xterm/parser/params.gd")
class TestParams:
extends 'res://addons/gut/test.gd'
var params
func before_each():
params = Params.new()
func test_respects_ctor_args():
params = Params.new(12, 23)
assert_eq(params.params.size(), 12)
assert_eq(params.sub_params.size(), 23)
assert_eq(params.to_array(), [])
func test_add_param():
params.add_param(1)
assert_eq(params.length, 1)
assert_eq(params.params.slice(0, params.length - 1), [1])
assert_eq(params.to_array(), [1])
params.add_param(23)
assert_eq(params.length, 2)
assert_eq(params.params.slice(0, params.length - 1), [1, 23])
assert_eq(params.to_array(), [1, 23])
assert_eq(params.sub_params_length, 0)
func test_add_sub_param():
params.add_param(1)
params.add_sub_param(2)
params.add_sub_param(3)
assert_eq(params.length, 1)
assert_eq(params.sub_params_length, 2)
assert_eq(params.to_array(), [1, [2, 3]])
params.add_param(12345)
params.add_sub_param(-1)
assert_eq(params.length, 2)
assert_eq(params.sub_params_length, 3)
assert_eq(params.to_array(), [1, [2,3], 12345, [-1]])
func test_should_not_add_sub_params_without_previous_param():
params.add_sub_param(2)
params.add_sub_param(3)
assert_eq(params.length, 0)
assert_eq(params.sub_params_length, 0)
assert_eq(params.to_array(), [])
params.add_param(1)
params.add_sub_param(2)
params.add_sub_param(3)
assert_eq(params.length, 1)
assert_eq(params.sub_params_length, 2)
assert_eq(params.to_array(), [1, [2, 3]])
func test_reset():
params.add_param(1)
params.add_sub_param(2)
params.add_sub_param(3)
params.add_param(12345)
params.reset()
assert_eq(params.length, 0)
assert_eq(params.sub_params_length, 0)
assert_eq(params.to_array(), [])
params.add_param(1)
params.add_sub_param(2)
params.add_sub_param(3)
params.add_param(12345)
params.add_sub_param(-1)
assert_eq(params.length, 2)
assert_eq(params.sub_params_length, 3)
assert_eq(params.to_array(), [1, [2, 3], 12345, [-1]])
func test_from_array_to_array():
var data = []
assert_eq(params.from_array(data).to_array(), data)
data = [1, [2, 3], 12345, [-1]]
assert_eq(params.from_array(data).to_array(), data)
data = [38, 2, 50, 100, 150]
assert_eq(params.from_array(data).to_array(), data)
data = [38, 2, 50, 100, [150]]
assert_eq(params.from_array(data).to_array(), data)
data = [38, [2, 50, 100, 150]]
assert_eq(params.from_array(data).to_array(), data)
# strip empty sub params
data = [38, [2, 50, 100, 150], 5, [], 6]
assert_eq(Params.from_array(data).to_array(), [38, [2, 50, 100, 150], 5, 6])
# ignore leading sub params
data = [[1,2], 12345, [-1]]
assert_eq(Params.from_array(data).to_array(), [12345, [-1]])
class TestParse:
extends 'res://addons/gut/test.gd'
var params
func parse(params, s):
params.reset()
params.add_param(0)
if typeof(s) == TYPE_STRING:
s = [s]
for chunk in s:
var i = 0
while i < chunk.length():
# Start for
var code = chunk.to_ascii()[i]
var do = true
while do:
match code:
0x3b:
params.add_param(0)
0x3a:
params.add_sub_param(-1)
_:
params.add_digit(code - 48)
code = chunk.to_ascii()[i] if i < chunk.length() else 0
i+=1
do = i < s.size() and code > 0x2f and code < 0x3c
i-=1
# End for
i+=1
func before_each():
params = Params.new()
func test_param_defaults_to_0(): # ZDM (Zero Default Mode)
parse(params, '')
assert_eq(params.to_array(), [0])
func test_sub_param_defaults_to_neg_1():
parse(params, ':')
assert_eq(params.to_array(), [0, [-1]])
func test_reset_on_new_sequence():
parse(params, '1;2;3')
assert_eq(params.to_array(), [1, 2, 3])
parse(params, '4')
assert_eq(params.to_array(), [4])
parse(params, '4::123:5;6;7')
assert_eq(params.to_array(), [4, [-1, 123, 5], 6, 7])
parse(params, '')
assert_eq(params.to_array(), [0])
func test_should_handle_length_restrictions_correctly():
params = Params.new(3, 3)
parse(params, '1;2;3')
assert_eq(params.to_array(), [1, 2, 3])
parse(params, '4')
assert_eq(params.to_array(), [4])
parse(params, '4::123:5;6;7')
assert_eq(params.to_array(), [4, [-1, 123, 5], 6, 7])
parse(params, '')
assert_eq(params.to_array(), [0])
# overlong params
parse(params, '4;38:2::50:100:150;48:5:22')
assert_eq(params.to_array(), [4, 38, [2, -1, 50], 48])
# overlong sub params
parse(params, '4;38:2::50:100:150;48:5:22')
assert_eq(params.to_array(), [4, 38, [2, -1, 50], 48])
func test_typical_sequences():
# SGR with semicolon syntax
parse(params, '0;4;38;2;50;100;150;48;5;22')
assert_eq(params.to_array(), [0, 4, 38, 2, 50, 100, 150, 48, 5, 22])
# SGR mixed style (partly wrong)
parse(params, '0;4;38;2;50:100:150;48;5:22')
assert_eq(params.to_array(), [0, 4, 38, 2, 50, [100, 150], 48, 5, [22]])
# SGR colon style
parse(params, '0;4;38:2::50:100:150;48:5:22')
assert_eq(params.to_array(), [0, 4, 38, [2, -1, 50, 100, 150], 48, [5, 22]])
func test_clamp_parsed_params():
parse(params, '2147483648')
assert_eq(params.to_array(), [0x7FFFFFFF])
func test_clamp_parsed_sub_params():
parse(params, ':2147483648')
assert_eq(params.to_array(), [0, [0x7FFFFFFF]])
func test_should_cancel_subdigits_if_beyond_params_limit():
parse(params, ';;;;;;;;;10;;;;;;;;;;20;;;;;;;;;;30;31;32;33;34;35::::::::')
assert_eq(params.to_array(), [
0, 0, 0, 0, 0, 0, 0, 0, 0, 10,
0, 0, 0, 0, 0, 0, 0, 0, 0, 20,
0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 31, 32
])
# func test_should_carry_forward_is_sub_state():
# parse(params, ['1:22:33', '44'])
# assert_eq(params.to_array(), [1, [22, 3344]])

View file

@ -0,0 +1,35 @@
# Copyright 2020 The GodotXterm authors. All rights reserved.
# License MIT
extends "res://addons/gut/test.gd"
const CanvasRenderingContext2D = preload("res://addons/godot_xterm/renderer/canvas_rendering_context_2d.gd")
const RegularFont = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres")
const BoldFont = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres")
var ctx
func before_each():
ctx = CanvasRenderingContext2D.new()
func test_measure_text():
assert_eq(ctx.measure_text("a").width, RegularFont.get_string_size("a").x)
func test_save_and_restore():
# fill_style
ctx.fill_style = Color.red
ctx.save()
ctx.fill_style = Color.blue
assert_eq(ctx.fill_style, Color.blue)
ctx.restore()
assert_eq(ctx.fill_style, Color.red)
# font
ctx.font = RegularFont
ctx.save()
ctx.font = BoldFont
assert_eq(ctx.font, BoldFont)
ctx.restore()
assert_eq(ctx.font, RegularFont)

View file

@ -0,0 +1,38 @@
# Copyright (c) 2018 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends "res://addons/gut/test.gd"
const CharacterJoinerRegistry = preload("res://addons/godot_xterm/renderer/character_joiner_registry.gd")
const Buffer = preload("res://addons/godot_xterm/buffer/buffer.gd")
const BufferLine = preload("res://addons/godot_xterm/buffer/buffer_line.gd")
const CircularList = preload("res://addons/godot_xterm/circular_list.gd")
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd")
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
const TestUtils = preload("res://test/test_utils.gd")
var registry
func line_data(data):
var tline = BufferLine.new(0)
for d in data:
var line = d[0]
var attr = d[1] if d.size() > 1 else 0
var offset = tline.length
tline.resize(tline.length + line.split('').size(), CellData.from_char_data([0, '', 0, 0]))
func before_each():
var buffer_service = TestUtils.MockBufferService.new(16, 10)
var lines = buffer_service.buffer.lines
lines.set_el(0, line_data([['a -> b -> c -> d']]))
lines.set_el(1, line_data([['a -> b => c -> d']]))
lines.set_el(2, line_data([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]))
registry = CharacterJoinerRegistry.new(buffer_service)
func test_has_no_joiners_upon_creation():
assert_eq(registry.get_joined_characters(0), [])

View file

@ -0,0 +1,218 @@
# Copyright (c) 2017 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends "res://addons/gut/test.gd"
const TestUtils = preload("res://test/test_utils.gd")
const InputHandler = preload("res://addons/godot_xterm/input_handler.gd")
const CharsetService = preload("res://addons/godot_xterm/services/charset_service.gd")
const Params = preload("res://addons/godot_xterm/parser/params.gd")
const CoreService = preload("res://addons/godot_xterm/services/core_service.gd")
var options_service
var buffer_service
var charset_service
var core_service
var input_handler
func get_lines(buffer_service, limit: int) -> Array:
var res = []
if not limit:
limit = buffer_service.rows
for i in range(limit):
var line = buffer_service.buffer.lines.get_el(i)
if line:
res.append(line.translate_to_string(true))
return res
func repeat(string: String, times: int) -> String:
var s = ""
for i in range(times):
s += string
return s
func term_content(buffer_service, trim: bool) -> PoolStringArray:
var result = PoolStringArray([])
for i in buffer_service.rows:
result.append(buffer_service.buffer.lines.get_el(i).translate_to_string(trim))
return result;
func before_each():
options_service = TestUtils.MockOptionsService.new()
buffer_service = TestUtils.MockBufferService.new(80, 30, options_service)
charset_service = CharsetService.new()
core_service = CoreService.new()
input_handler = InputHandler.new(buffer_service, core_service, charset_service, options_service)
# Skipping lots of tests here...
func test_erase_in_line():
buffer_service = TestUtils.MockBufferService.new(10, 3, options_service)
input_handler = InputHandler.new(buffer_service, core_service, charset_service, options_service)
# fill 6 lines to test 3 different states
input_handler.parse(repeat("a", buffer_service.cols))
input_handler.parse(repeat("a", buffer_service.cols))
input_handler.parse(repeat("a", buffer_service.cols))
# params[0] - right erase
buffer_service.buffer.y = 0
buffer_service.buffer.x = 7
input_handler.erase_in_line(Params.from_array([0]))
assert_eq(buffer_service.buffer.lines.get_el(0).translate_to_string(false),
repeat("a", 7) + " ")
# # params[1] - left erase
# buffer_service.buffer.y = 1
# buffer_service.buffer.x = 70
# input_handler.erase_in_line(Params.from_array([1]))
# assert_eq(buffer_service.buffer.lines.get_el(1).translate_to_string(false),
# repeat(" ", 70) + " aaaaaaaaa")
#
# # params[1] - left erase
# buffer_service.buffer.y = 2
# buffer_service.buffer.x = 70
# input_handler.erase_in_line(Params.from_array([2]))
# assert_eq(buffer_service.buffer.lines.get_el(2).translate_to_string(false),
# repeat(" ", buffer_service.cols))
func skip_test_erase_in_display():
buffer_service = TestUtils.MockBufferService.new(80, 7, options_service)
input_handler = InputHandler.new(buffer_service, core_service, charset_service, options_service)
# fill display with a's
for _i in range(buffer_service.rows):
input_handler.parse(repeat("a", buffer_service.cols))
# params [0] - right and below erase
buffer_service.buffer.y = 5
buffer_service.buffer.x = 40
input_handler.erase_in_display(Params.from_array([0]))
assert_eq(term_content(buffer_service, false), PoolStringArray([
repeat("a", buffer_service.cols),
repeat("a", buffer_service.cols),
repeat("a", buffer_service.cols),
repeat("a", buffer_service.cols),
repeat("a", buffer_service.cols),
repeat("a", 40) + repeat(" ", buffer_service.cols - 40),
repeat(" ", buffer_service.cols),
]))
assert_eq(term_content(buffer_service, true), PoolStringArray([
repeat("a", buffer_service.cols),
repeat("a", buffer_service.cols),
repeat("a", buffer_service.cols),
repeat("a", buffer_service.cols),
repeat("a", buffer_service.cols),
repeat("a", 40),
""
]))
# reset
for _i in range(buffer_service.rows):
input_handler.parse(repeat("a", buffer_service.cols))
# params [1] - left and above
buffer_service.buffer.y = 5;
buffer_service.buffer.x = 40;
input_handler.erase_in_display(Params.from_array([1]))
assert_eq(term_content(buffer_service, false), PoolStringArray([
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
repeat(" ", 41) + repeat("a", buffer_service.cols - 41),
repeat("a", buffer_service.cols),
]))
assert_eq(term_content(buffer_service, true), PoolStringArray([
"",
"",
"",
"",
"",
repeat(" ", 41) + repeat("a", buffer_service.cols - 41),
repeat("a", buffer_service.cols),
]))
# reset
for _i in range(buffer_service.rows):
input_handler.parse(repeat("a", buffer_service.cols))
# params [2] - whole screen
buffer_service.buffer.y = 5;
buffer_service.buffer.x = 40;
input_handler.erase_in_display(Params.from_array([2]));
assert_eq(term_content(buffer_service, false), PoolStringArray([
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
repeat(" ", buffer_service.cols),
]))
assert_eq(term_content(buffer_service, true), PoolStringArray([
"",
"",
"",
"",
"",
"",
"",
]))
#
# # reset and add a wrapped line
# buffer_service.buffer.y = 0;
# buffer_service.buffer.x = 0;
# input_handler.parse(Array(buffer_service.cols + 1).join('a')); # line 0
# input_handler.parse(Array(buffer_service.cols + 10).join('a')); # line 1 and 2
# for (let i = 3; i < buffer_service.rows; ++i) input_handler.parse(Array(buffer_service.cols + 1).join('a'));
#
# # params[1] left and above with wrap
# # confirm precondition that line 2 is wrapped
# expect(buffer_service.buffer.lines.get(2)!.isWrapped).true;
# buffer_service.buffer.y = 2;
# buffer_service.buffer.x = 40;
# input_handler.erase_in_display(Params.from_array([1]));
# expect(buffer_service.buffer.lines.get(2)!.isWrapped).false;
#
# # reset and add a wrapped line
# buffer_service.buffer.y = 0;
# buffer_service.buffer.x = 0;
# input_handler.parse(Array(buffer_service.cols + 1).join('a')); # line 0
# input_handler.parse(Array(buffer_service.cols + 10).join('a')); # line 1 and 2
# for (let i = 3; i < buffer_service.rows; ++i) input_handler.parse(Array(buffer_service.cols + 1).join('a'));
#
# # params[1] left and above with wrap
# # confirm precondition that line 2 is wrapped
# expect(buffer_service.buffer.lines.get(2)!.isWrapped).true;
# buffer_service.buffer.y = 1;
# buffer_service.buffer.x = 90; # Cursor is beyond last column
# input_handler.erase_in_display(Params.from_array([1]));
# expect(buffer_service.buffer.lines.get(2)!.isWrapped).false;
func test_print_does_not_cause_an_infinite_loop():
var container = []
container.resize(10)
container[0] = 0x200B
input_handler.print(container, 0, 1)
# FIXME
func skip_test_clear_cells_to_the_right_on_early_wrap_around():
buffer_service.resize(5, 5)
options_service.options.scrollback = 1
input_handler.parse('12345')
buffer_service.buffer.x = 0
input_handler.parse("¥¥¥")
assert_eq(get_lines(buffer_service, 2), PoolStringArray(["¥¥", ""]))