mirror of
https://github.com/lihop/godot-xterm.git
synced 2024-11-22 09:40:25 +01:00
Add/update more files
This commit is contained in:
parent
3307231b65
commit
0769592a1b
44 changed files with 4188 additions and 362 deletions
BIN
.test.txt.swp
Normal file
BIN
.test.txt.swp
Normal file
Binary file not shown.
127
addons/godot_xterm/buffer/attribute_data.gd
Normal file
127
addons/godot_xterm/buffer/attribute_data.gd
Normal 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
|
141
addons/godot_xterm/buffer/buffer.gd
Normal file
141
addons/godot_xterm/buffer/buffer.gd
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
247
addons/godot_xterm/buffer/buffer_line.gd
Normal file
247
addons/godot_xterm/buffer/buffer_line.gd
Normal 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
|
55
addons/godot_xterm/buffer/buffer_set.gd
Normal file
55
addons/godot_xterm/buffer/buffer_set.gd
Normal 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)
|
68
addons/godot_xterm/buffer/cell_data.gd
Normal file
68
addons/godot_xterm/buffer/cell_data.gd
Normal 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()]
|
93
addons/godot_xterm/buffer/constants.gd
Normal file
93
addons/godot_xterm/buffer/constants.gd
Normal 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
|
||||||
|
}
|
75
addons/godot_xterm/circular_list.gd
Normal file
75
addons/godot_xterm/circular_list.gd
Normal 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
|
||||||
|
|
108
addons/godot_xterm/color_manager.gd
Normal file
108
addons/godot_xterm/color_manager.gd
Normal 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()
|
||||||
|
|
||||||
|
|
||||||
|
|
24
addons/godot_xterm/data/charsets.gd
Normal file
24
addons/godot_xterm/data/charsets.gd
Normal 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']
|
|
@ -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 )
|
||||||
|
|
|
@ -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()
|
||||||
|
|
1036
addons/godot_xterm/input_handler.gd
Normal file
1036
addons/godot_xterm/input_handler.gd
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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():
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
|
||||||
|
# License MIT
|
||||||
tool
|
tool
|
||||||
extends EditorPlugin
|
extends EditorPlugin
|
||||||
|
|
||||||
|
|
250
addons/godot_xterm/renderer/base_render_layer.gd
Normal file
250
addons/godot_xterm/renderer/base_render_layer.gd
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
74
addons/godot_xterm/renderer/canvas_rendering_context_2d.gd
Normal file
74
addons/godot_xterm/renderer/canvas_rendering_context_2d.gd
Normal 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
|
46
addons/godot_xterm/renderer/character_joiner_registry.gd
Normal file
46
addons/godot_xterm/renderer/character_joiner_registry.gd
Normal 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
|
118
addons/godot_xterm/renderer/renderer.gd
Normal file
118
addons/godot_xterm/renderer/renderer.gd
Normal 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
191
addons/godot_xterm/renderer/text_render_layer.gd
Normal file
191
addons/godot_xterm/renderer/text_render_layer.gd
Normal 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
|
39
addons/godot_xterm/services/buffer_service.gd
Normal file
39
addons/godot_xterm/services/buffer_service.gd
Normal 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)
|
30
addons/godot_xterm/services/charset_service.gd
Normal file
30
addons/godot_xterm/services/charset_service.gd
Normal 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
|
27
addons/godot_xterm/services/core_service.gd
Normal file
27
addons/godot_xterm/services/core_service.gd
Normal 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()
|
53
addons/godot_xterm/services/options_service.gd
Normal file
53
addons/godot_xterm/services/options_service.gd
Normal 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
|
|
@ -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()
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
39
scenes/showcase.tscn
Normal 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 = {
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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
66
test/test_utils.gd
Normal 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
|
BIN
test/unit/.test_input_handler.gd.swp
Normal file
BIN
test/unit/.test_input_handler.gd.swp
Normal file
Binary file not shown.
128
test/unit/buffer/test_buffer.gd
Normal file
128
test/unit/buffer/test_buffer.gd
Normal 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
348
test/unit/buffer/test_buffer_line.gd
Normal file
348
test/unit/buffer/test_buffer_line.gd
Normal 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, '1', 2, '1'.ord_at(0)])
|
||||||
|
assert_eq(cell.get_as_char_data(), [123, '1', 2, '1'.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), '')
|
|
@ -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:
|
|
@ -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)
|
202
test/unit/parser/test_parser.gd
Normal file
202
test/unit/parser/test_parser.gd
Normal 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]])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
35
test/unit/renderer/test_canvas_rendering_context_2d.gd
Normal file
35
test/unit/renderer/test_canvas_rendering_context_2d.gd
Normal 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)
|
38
test/unit/renderer/test_character_joiner_registry.gd
Normal file
38
test/unit/renderer/test_character_joiner_registry.gd
Normal 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), [])
|
218
test/unit/test_input_handler.gd
Normal file
218
test/unit/test_input_handler.gd
Normal 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(["¥¥", "¥"]))
|
Loading…
Reference in a new issue