mirror of
https://github.com/lihop/godot-xterm.git
synced 2024-11-22 17:50:25 +01:00
0d4e10f5ab
Most notably: - Reflow is now working. Terminal size will fill the window and cols/rows will be resized/calculated based on window and font size. - Added support for different fonts (i.e. bold, italic, bolditalic). - Enabled blinking characters. - Adde more tests and caught a few subtle bugs. - Removed renderer code (which was part of xterm.js) and just doing naive rendering in terminal.gd, but it seems to perform a lot faster. Still not working completely: - vim (some weirdness going on). - vttest (more weirdness). Todo: - Fix the above. - Draw the cursor! - Improve performance. Performance is still not great. The terminal becomes unusable when running `yes` or `cmatrix -r`.
436 lines
15 KiB
GDScript
436 lines
15 KiB
GDScript
# 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)],
|
||
])
|
||
|
||
|
||
func test_copy_from():
|
||
var line = BufferLineTest.new(5)
|
||
line.set_cell(0, CellData.from_char_data([1, 'a', 0, 'a'.ord_at(0)]))
|
||
line.set_cell(0, CellData.from_char_data([2, 'b', 0, 'b'.ord_at(0)]))
|
||
line.set_cell(0, CellData.from_char_data([3, 'c', 0, 'c'.ord_at(0)]))
|
||
line.set_cell(0, CellData.from_char_data([4, 'd', 0, 'd'.ord_at(0)]))
|
||
line.set_cell(0, CellData.from_char_data([5, 'e', 0, 'e'.ord_at(0)]))
|
||
var line2 = BufferLineTest.new(5, CellData.from_char_data([1, 'a', 0, 'a'.ord_at(0)]), true)
|
||
line2.copy_from(line)
|
||
assert_eq(line2.to_array(), line.to_array())
|
||
assert_eq(line2.length, line.length)
|
||
assert_eq(line2.is_wrapped, line.is_wrapped)
|
||
|
||
|
||
class TestResize:
|
||
extends "res://addons/gut/test.gd"
|
||
|
||
|
||
var CHAR_DATA = [1, 'a', 0, 'a'.ord_at(0)]
|
||
var line
|
||
|
||
func repeat(el, times: int) -> Array:
|
||
var result = []
|
||
result.resize(times)
|
||
for i in range(times):
|
||
result[i] = el
|
||
return result
|
||
|
||
|
||
func test_enlarge():
|
||
line = BufferLineTest.new(5, CellData.from_char_data(CHAR_DATA), false)
|
||
line.resize(10, CellData.from_char_data(CHAR_DATA))
|
||
assert_eq(line.to_array(), repeat(CHAR_DATA, 10))
|
||
|
||
|
||
func test_shrink():
|
||
line = BufferLineTest.new(10, CellData.from_char_data(CHAR_DATA), false)
|
||
line.resize(5, CellData.from_char_data(CHAR_DATA))
|
||
assert_eq(line.to_array(), repeat(CHAR_DATA, 5))
|
||
|
||
|
||
func test_shrink_to_0_length():
|
||
line = BufferLineTest.new(10, CellData.from_char_data(CHAR_DATA), false)
|
||
line.resize(0, CellData.from_char_data(CHAR_DATA))
|
||
assert_eq(line.to_array(), repeat(CHAR_DATA, 0))
|
||
|
||
func shrink_then_enlarge():
|
||
line = BufferLineTest.new(10, CellData.from_char_data(CHAR_DATA), false);
|
||
line.set_cell(2, CellData.from_char_data([0, '😁', 1, '😁'.ord_at(0)]))
|
||
line.set_cell(9, CellData.from_char_data([0, '😁', 1, '😁'.ord_at(0)]))
|
||
assert_eq(line.translate_to_string(), 'aa😁aaaaaa😁')
|
||
line.resize(5, CellData.from_char_data(CHAR_DATA))
|
||
assert_eq(line.translate_to_string(), 'aa😁aa')
|
||
line.resize(10, CellData.from_char_data(CHAR_DATA))
|
||
assert_eq(line.translate_to_string(), 'aa😁aaaaaaa')
|
||
|
||
|
||
class TestTrimLength:
|
||
extends "res://addons/gut/test.gd"
|
||
|
||
|
||
var line
|
||
|
||
|
||
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]))
|
||
|
||
|
||
func test_empty_line():
|
||
assert_eq(line.get_trimmed_length(), 0)
|
||
|
||
|
||
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)]))
|
||
assert_eq(line.get_trimmed_length(), 3)
|
||
|
||
|
||
func test_unicode():
|
||
line.set_cell(0, CellData.from_char_data([1, "\u1f914", 1, "\u1f914".ord_at(0)]))
|
||
line.set_cell(2, CellData.from_char_data([1, "\u1f914", 1, "\u1f914".ord_at(0)]))
|
||
assert_eq(line.get_trimmed_length(), 3)
|
||
|
||
|
||
func test_one_cell():
|
||
line.set_cell(0, CellData.from_char_data([1, "a", 1, "a".ord_at(0)]))
|
||
assert_eq(line.get_trimmed_length(), 1)
|
||
|
||
|
||
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 TestTranslateToString:
|
||
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), '')
|