From 0769592a1b1e1388032abcd67291576eead8e936 Mon Sep 17 00:00:00 2001 From: Leroy Hopson Date: Sun, 10 May 2020 22:56:49 +1200 Subject: [PATCH] Add/update more files --- .test.txt.swp | Bin 0 -> 12288 bytes addons/godot_xterm/buffer/attribute_data.gd | 127 ++ addons/godot_xterm/buffer/buffer.gd | 141 +++ addons/godot_xterm/buffer/buffer_line.gd | 247 ++++ addons/godot_xterm/buffer/buffer_set.gd | 55 + addons/godot_xterm/buffer/cell_data.gd | 68 ++ addons/godot_xterm/buffer/constants.gd | 93 ++ addons/godot_xterm/circular_list.gd | 75 ++ addons/godot_xterm/color_manager.gd | 108 ++ addons/godot_xterm/data/charsets.gd | 24 + .../source_code_pro_regular.tres | 1 + addons/godot_xterm/input/text_decoder.gd | 9 +- addons/godot_xterm/input_handler.gd | 1036 +++++++++++++++++ .../parser/escape_sequence_parser.gd | 2 +- addons/godot_xterm/parser/params.gd | 17 +- addons/godot_xterm/parser/transition_table.gd | 2 +- addons/godot_xterm/plugin.gd | 2 + .../godot_xterm/renderer/base_render_layer.gd | 250 ++++ .../renderer/canvas_rendering_context_2d.gd | 74 ++ .../renderer/character_joiner_registry.gd | 46 + addons/godot_xterm/renderer/renderer.gd | 118 ++ .../godot_xterm/renderer/text_render_layer.gd | 191 +++ addons/godot_xterm/services/buffer_service.gd | 39 + .../godot_xterm/services/charset_service.gd | 30 + addons/godot_xterm/services/core_service.gd | 27 + .../godot_xterm/services/options_service.gd | 53 + addons/godot_xterm/terminal.gd | 502 +++----- project.godot | 21 +- scenes/demo.gd | 34 +- scenes/demo.tscn | 30 +- scenes/showcase.tscn | 39 + test/integration/test_terminal.gd | 6 +- test/test.tscn | 4 +- test/test_utils.gd | 66 ++ test/unit/.test_input_handler.gd.swp | Bin 0 -> 16384 bytes test/unit/buffer/test_buffer.gd | 128 ++ test/unit/buffer/test_buffer_line.gd | 348 ++++++ test/unit/{ => input}/test_text_decoder.gd | 12 +- test/unit/{ => parser}/test_dcs_parser.gd | 0 .../test_escape_sequence_parser.gd | 32 +- test/unit/parser/test_parser.gd | 202 ++++ .../test_canvas_rendering_context_2d.gd | 35 + .../test_character_joiner_registry.gd | 38 + test/unit/test_input_handler.gd | 218 ++++ 44 files changed, 4188 insertions(+), 362 deletions(-) create mode 100644 .test.txt.swp create mode 100644 addons/godot_xterm/buffer/attribute_data.gd create mode 100644 addons/godot_xterm/buffer/buffer.gd create mode 100644 addons/godot_xterm/buffer/buffer_line.gd create mode 100644 addons/godot_xterm/buffer/buffer_set.gd create mode 100644 addons/godot_xterm/buffer/cell_data.gd create mode 100644 addons/godot_xterm/buffer/constants.gd create mode 100644 addons/godot_xterm/circular_list.gd create mode 100644 addons/godot_xterm/color_manager.gd create mode 100644 addons/godot_xterm/data/charsets.gd create mode 100644 addons/godot_xterm/input_handler.gd create mode 100644 addons/godot_xterm/renderer/base_render_layer.gd create mode 100644 addons/godot_xterm/renderer/canvas_rendering_context_2d.gd create mode 100644 addons/godot_xterm/renderer/character_joiner_registry.gd create mode 100644 addons/godot_xterm/renderer/renderer.gd create mode 100644 addons/godot_xterm/renderer/text_render_layer.gd create mode 100644 addons/godot_xterm/services/buffer_service.gd create mode 100644 addons/godot_xterm/services/charset_service.gd create mode 100644 addons/godot_xterm/services/core_service.gd create mode 100644 addons/godot_xterm/services/options_service.gd create mode 100644 scenes/showcase.tscn create mode 100644 test/test_utils.gd create mode 100644 test/unit/.test_input_handler.gd.swp create mode 100644 test/unit/buffer/test_buffer.gd create mode 100644 test/unit/buffer/test_buffer_line.gd rename test/unit/{ => input}/test_text_decoder.gd (96%) rename test/unit/{ => parser}/test_dcs_parser.gd (100%) rename test/unit/{ => parser}/test_escape_sequence_parser.gd (93%) create mode 100644 test/unit/parser/test_parser.gd create mode 100644 test/unit/renderer/test_canvas_rendering_context_2d.gd create mode 100644 test/unit/renderer/test_character_joiner_registry.gd create mode 100644 test/unit/test_input_handler.gd diff --git a/.test.txt.swp b/.test.txt.swp new file mode 100644 index 0000000000000000000000000000000000000000..32eafcc59e5568eafbbdc37082b988de16490fff GIT binary patch literal 12288 zcmeI%yAFad6oBF4ZZ!G=sI%N$d<6$b+*KOI8wAdPi$1B3Vj>`nV!}r9H))&RPENnA z>CV}#*5=Z3RbGZg^DUOA)I}%Jy`E_3yz1td1qGtawM z5I_I{1Q0*~0R#|00D(%N__coxlzQj2`2Vlp|K%7Z1Q0*~0R#|0 S009ILKmY**5a_mJAe literal 0 HcmV?d00001 diff --git a/addons/godot_xterm/buffer/attribute_data.gd b/addons/godot_xterm/buffer/attribute_data.gd new file mode 100644 index 0000000..d22bd04 --- /dev/null +++ b/addons/godot_xterm/buffer/attribute_data.gd @@ -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 diff --git a/addons/godot_xterm/buffer/buffer.gd b/addons/godot_xterm/buffer/buffer.gd new file mode 100644 index 0000000..19a3321 --- /dev/null +++ b/addons/godot_xterm/buffer/buffer.gd @@ -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 + + + + + diff --git a/addons/godot_xterm/buffer/buffer_line.gd b/addons/godot_xterm/buffer/buffer_line.gd new file mode 100644 index 0000000..c124592 --- /dev/null +++ b/addons/godot_xterm/buffer/buffer_line.gd @@ -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 diff --git a/addons/godot_xterm/buffer/buffer_set.gd b/addons/godot_xterm/buffer/buffer_set.gd new file mode 100644 index 0000000..e8f0790 --- /dev/null +++ b/addons/godot_xterm/buffer/buffer_set.gd @@ -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) diff --git a/addons/godot_xterm/buffer/cell_data.gd b/addons/godot_xterm/buffer/cell_data.gd new file mode 100644 index 0000000..b2ec880 --- /dev/null +++ b/addons/godot_xterm/buffer/cell_data.gd @@ -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()] diff --git a/addons/godot_xterm/buffer/constants.gd b/addons/godot_xterm/buffer/constants.gd new file mode 100644 index 0000000..b5521d3 --- /dev/null +++ b/addons/godot_xterm/buffer/constants.gd @@ -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 +} diff --git a/addons/godot_xterm/circular_list.gd b/addons/godot_xterm/circular_list.gd new file mode 100644 index 0000000..81c154f --- /dev/null +++ b/addons/godot_xterm/circular_list.gd @@ -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 + diff --git a/addons/godot_xterm/color_manager.gd b/addons/godot_xterm/color_manager.gd new file mode 100644 index 0000000..6f7cd97 --- /dev/null +++ b/addons/godot_xterm/color_manager.gd @@ -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() + + + diff --git a/addons/godot_xterm/data/charsets.gd b/addons/godot_xterm/data/charsets.gd new file mode 100644 index 0000000..e8d4954 --- /dev/null +++ b/addons/godot_xterm/data/charsets.gd @@ -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'] diff --git a/addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres b/addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres index 22109c0..8036b84 100644 --- a/addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres +++ b/addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres @@ -3,4 +3,5 @@ [ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.ttf" type="DynamicFontData" id=1] [resource] +size = 20 font_data = ExtResource( 1 ) diff --git a/addons/godot_xterm/input/text_decoder.gd b/addons/godot_xterm/input/text_decoder.gd index 5d71cd5..6ea7155 100644 --- a/addons/godot_xterm/input/text_decoder.gd +++ b/addons/godot_xterm/input/text_decoder.gd @@ -28,22 +28,19 @@ static func utf32_to_utf8(codepoint: int): 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. -# 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). static func utf32_to_string(data: Array, start: int = 0, end: int = -1): if end == -1: end = data.size() var result = '' for i in range(start, end): - result += string_from_codepoint(data[i]) + result += char(data[i]) return result + # Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints. class Utf8ToUtf32: var interim = PoolByteArray() diff --git a/addons/godot_xterm/input_handler.gd b/addons/godot_xterm/input_handler.gd new file mode 100644 index 0000000..f221ebe --- /dev/null +++ b/addons/godot_xterm/input_handler.gd @@ -0,0 +1,1036 @@ +# Copyright (c) 2014 The xterm.js authors. All rights reserved. +# Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) +# Ported to GDScript by the GodotXterm authors. +# License MIT +extends Reference + +const Constants = preload("res://addons/godot_xterm/parser/constants.gd") +const BufferConstants = preload("res://addons/godot_xterm/buffer/constants.gd") +const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd") +const CellData = preload("res://addons/godot_xterm/buffer/cell_data.gd") +const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd") +const EscapeSequenceParser = preload("res://addons/godot_xterm/parser/escape_sequence_parser.gd") +const Charsets = preload("res://addons/godot_xterm/data/charsets.gd") + +const Attributes = BufferConstants.Attributes +const C0 = Constants.C0 +const C1 = Constants.C1 +const FgFlags = BufferConstants.FgFlags +const BgFlags = BufferConstants.BgFlags +const UnderlineStyle = BufferConstants.UnderlineStyle + +const GLEVEL = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2} +const MAX_PARSEBUFFER_LENGTH = 131072 +const STACK_LIMIT = 10 +var DEFAULT_ATTRIBUTE_DATA = AttributeData.new() + +signal line_fed +signal cursor_moved +signal bell_requested +signal refresh_rows_requested(start_row, end_row) +signal reset_requested +signal scroll_requested +signal windows_options_report_requested +signal scrollbar_sync_requested + +var _buffer_service +var _core_service +var _charset_service +var _options_service +var _parser + +var _parse_buffer: Array = [] +var _utf8_decoder = Decoder.Utf8ToUtf32.new() +var _cur_attr_data = AttributeData.new() +var _erase_attr_data_internal = AttributeData.new() +var _work_cell = CellData.new() +var _parse_thread: Thread = Thread.new() + +var _buffer setget ,_get_buffer +var buffer setget _set_buffer,_get_buffer + + +func _set_buffer(buffer) -> void: + buffer = buffer + + +func _get_buffer(): + return _buffer_service.buffer + + +func _init(buffer_service, core_service, charset_service, options_service, + parser = EscapeSequenceParser.new()): + _buffer_service = buffer_service + _core_service = core_service + _charset_service = charset_service + _options_service = options_service + _parser = parser + + buffer = _buffer_service.buffer + _buffer_service.connect("buffer_activated", self, "_set_buffer") + + + # Print handler + _parser.set_print_handler(self, "print") + + # Execute handlers + _parser.set_execute_handler(C0.BEL, self, 'bell') + _parser.set_execute_handler(C0.LF, self, 'line_feed') + _parser.set_execute_handler(C0.VT, self, 'line_feed') + _parser.set_execute_handler(C0.FF, self, 'line_feed') + _parser.set_execute_handler(C0.CR, self, 'carriage_return') + _parser.set_execute_handler(C0.BS, self, 'backspace') + _parser.set_execute_handler(C0.HT, self, '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 + _parser.set_csi_handler({'final': '@'}, self, 'insert_chars') + _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_char_absolute') + _parser.set_csi_handler({'final': 'H'}, self, '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 parse(data) -> void: +# if _parse_thread.is_active(): +# _parse_thread.wait_to_finish() +# _parse_thread.start(self, "_parse_async", data) + + +func parse(data) -> void: + var buffer = _buffer_service.buffer + var cursor_start_x = buffer.x + var cursor_start_y = buffer.y + + var data_length = data.length() if typeof(data) == TYPE_STRING else data.size() + + # resize input buffer if needed + if _parse_buffer.size() < data_length and _parse_buffer.size() < MAX_PARSEBUFFER_LENGTH: + _parse_buffer.resize(min(data_length, MAX_PARSEBUFFER_LENGTH)) + + # process big data in smaller chunks + if data_length > MAX_PARSEBUFFER_LENGTH: + var i = 0 + while i < data_length: + var end = i + MAX_PARSEBUFFER_LENGTH if i + MAX_PARSEBUFFER_LENGTH < data_length else data_length + var length + match typeof(data): + TYPE_STRING: + length = _utf8_decoder.decode(data.to_utf8(), _parse_buffer) + TYPE_RAW_ARRAY: + length = _utf8_decoder.decode(data, _parse_buffer) + TYPE_ARRAY: + length = data.size() + _parse_buffer = data.duplicate() + _parser.parse(_parse_buffer, length) + i += MAX_PARSEBUFFER_LENGTH + else: + var length + match typeof(data): + TYPE_STRING: + length = _utf8_decoder.decode(data.to_utf8(), _parse_buffer) + TYPE_RAW_ARRAY: + length = _utf8_decoder.decode(data, _parse_buffer) + TYPE_ARRAY: + length = data.size() + _parse_buffer = data.duplicate() + _parser.parse(_parse_buffer, length) + + buffer = _buffer_service.buffer + if (buffer.x != cursor_start_x or buffer.y != cursor_start_y): + emit_signal("cursor_moved") + + # Refresh all rows. + emit_signal("refresh_rows_requested") + # TODO: Refresh only dirty rows accumulated as part of parsing. + + +func _exit_tree(): + _parse_thread.wait_to_finish() + + +func print(data: Array, start: int, end: int) -> void: + var code: int + var ch_width: int + var buffer = _buffer_service.buffer + var charset = _charset_service.charset + var screen_reader_mode = _options_service.options.screen_reader_mode + var cols = _buffer_service.cols + var wraparound_mode = true #TODO _core_service.modes.wraparound + var insert_mode = false # TODO FIXME! _core_service.modes.insert_mode + var cur_attr = _cur_attr_data + var buffer_row = buffer.lines.get_el(buffer.ybase + buffer.y) + + # TODO: dirtyRowService stuff + + # handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char + if buffer.x and end - start > 0 and buffer_row.get_width(buffer.x - 1) == 2: + buffer_row.set_cell_from_codepoint(buffer.x - 1, 0, 1, cur_attr.fg, cur_attr.bg, cur_attr.extended) + + for pos in range(start, end): + code = data[pos] + + # calculate print space + # expensive call, therefore we save width in line buffer + ch_width = char(code).length() # FIXME + + # get charset replacement character + # charset is only defined for ASCII, therefore we only + # search for an replacement char if code < 127 + if code < 127 and charset: + var ch = charset[char(code)] + if ch: + code = ch.ord_at(0) + + if screen_reader_mode: + pass + # TODO: Handle A11y + + # insert combining char at last cursor position + # buffer.x should never be 0 for a combining char + # since they always follow a cell consuming char + # therefore we can test for buffer.x to avoid oveflow left + if (not ch_width) and buffer.x: + if not buffer_row.get_width(buffer.x - 1): + # found empty cell after full_width, need to go 2 cells back + # it is save to step 2 cells back here + # since an empty cell is only set by full_width chars + buffer_row.add_codepoint_to_cell(buffer.x - 2, code) + else: + buffer_row.add_codepoint_to_cell(buffer.x - 1, code) + continue + + # goto next line if ch would overflow + # NOTE: To avoid costly width checks here, + # the terminal does not allow a cols < 2 + if buffer.x + ch_width - 1 >= cols: + # autowrap - DECAWM + # automatically wraps to the beginning of the next line + if wraparound_mode: + while buffer.x < cols: + buffer_row.set_cell_from_codepoint(buffer.x, 0, 1, cur_attr.fg, cur_attr.bg, cur_attr.extended) + buffer.x += 1 + buffer.x = 0 + buffer.y += 1 + if buffer.y == buffer.scroll_bottom + 1: + buffer.y -= 1 + emit_signal("scroll_requested", _erase_attr_data(), true) + else: + if buffer.y >= _buffer_service.rows: + buffer.y = _buffer_service.rows - 1 + # The line already exists (e.g. the initial viewport), mark it as a + # wrapped line + buffer.lines.get_el(buffer.ybase + buffer.y).is_wrapped = true + # row changed, get it again + buffer_row = buffer.lines.get_el(buffer.ybase + buffer.y) + else: + buffer.x = cols - 1 + if ch_width == 2: + # FIXME: check for xterm behavior + # What to do here? We got a wide char that does not fit into last cell + continue + + # insert mode: move characters to right + if insert_mode: + # right shift cells according to the width + buffer_row.insert_cells(buffer.x, ch_width, buffer.get_null_cell(cur_attr), cur_attr) + # test last cell - since the last cell has only room for + # a halfwidth char any fullwidth shifted there is lost + # and will be set to empty cell + if buffer_row.get_width(cols - 1) == 2: + buffer_row.set_cell_from_codepoint(cols - 1, Constants.NULL_CELL_CODE, Constants.NULL_CELL_WIDTH, cur_attr.fg, cur_attr.bg, cur_attr.extended) + + # write current char to buffer and advance cursor + buffer_row.set_cell_from_codepoint(buffer.x, code, ch_width, cur_attr.fg, cur_attr.bg, cur_attr.extended) + buffer.x += 1 + + # fullwidth char - also set next cell to placeholder stub and advance cursor + # for graphemes bigger than fullwidth we can simply loop to zero + # we already made sure above, that buffer.x + ch_width will not overflow right + if ch_width > 0: + ch_width -= 1 + while ch_width: + # other than a regular empty cell a cell following a wide char has no width + buffer_row.set_cell_from_codepoint(buffer.x, 0, 0, cur_attr.fg, cur_attr.bg, cur_attr.extended) + buffer.x += 1 + ch_width -= 1 + + # Store last char in Parser.preceding_codepoint for REP to work correctly + # This needs to check whether: + # - fullwidth + surrogates: reset + # - combining: only base char gets carried on (bug in xterm?) + if end - start > 0: + buffer_row.load_cell(buffer.x - 1, _work_cell) + if _work_cell.get_width() == 2 or _work_cell.get_code() > 0xFFFF: + _parser.preceding_codepoint = 0 + elif _work_cell.is_combined(): + _parser.preceding_codepoint = _work_cell.get_chars().ord_at(0) + else: + _parser.preceding_codepoint = _work_cell.content + + # handle wide chars: reset cell to the right if it is second cell of a wide char + if buffer.x < cols and end - start > 0 and buffer_row.get_width(buffer.x) == 0 and not buffer_row.has_content(buffer.x): + buffer_row.set_cell_from_codepoint(buffer.x, 0, 1, cur_attr.fg, cur_attr.bg, cur_attr.extended) + + # TODO dirty row stuff + # _dirty_row_service.mark_dirty(buffer.y) + + +func bell(): + emit_signal("bell_requested") + + +func line_feed(): + var buffer = _buffer_service.buffer + + if _options_service.options.convert_eol: + buffer.x = 0 + buffer.y += 1 + if buffer.y == buffer.scroll_bottom + 1: + buffer.y -= 1 + emit_signal("scroll_requested") + elif buffer.y >= _buffer_service.rows: + buffer.y = _buffer_service.rows - 1 + # If the end of the line is hit, prevent this action from wrapping around to the next line. + if buffer.x >= _buffer_service.cols: + buffer.x -= 1 + + emit_signal("line_fed") + + +func carriage_return(): + _buffer_service.buffer.x = 0 + + +func backspace(): + var buffer = _buffer_service.buffer + + # reverse wrap-around is disabled + if not _core_service.dec_private_modes.reverse_wraparound: + _restrict_cursor() + if buffer.x > 0: + buffer.x -= 1 + return + + # reverse wrap-around is enabled + # other than for normal operation mode, reverse wrap-around allows the cursor + # to be at x=cols to be able to address the last cell of a row by BS + _restrict_cursor(_buffer_service.cols) + + if buffer.x > 0: + buffer.x -= 1 + else: + # reverse wrap-around handling: + # Our implementation deviates from xterm on purpose. Details: + # - only previous soft NLs can be reversed (is_wrapped=true) + # - only works within scrollborders (top/bottom, left/right not yet supported) + # - cannot peek into scrollbuffer + # - any cursor movement sequence keeps working as expected + if buffer.x == 0 \ + and buffer.y > buffer.scroll_top \ + and buffer.y <= buffer.scroll_bottom \ + and buffer.lines.get_el(buffer.ybase + buffer.y).is_wrapped: + buffer.lines.get_el(buffer.ybase + buffer.y).is_wrapped = false + buffer.y -= 1 + buffer.x = _buffer_service.cols - 1 + # find last taken cell - last can have 3 different states: + # - has_content(true) + has_width(1): narrow char - we are done + # - has_width(0): second part of a wide char - we are done + # - has_content(false) + has_width(1): empty cell due to early wrapping wide char, go one cell further back + var line = buffer.lines.get_el(buffer.ybase + buffer.y) + if line.has_width(buffer.x) and line.has_content(buffer.x): + buffer.x -= 1 + # We do this only once, since width=1 + has_content= false currently happens only once before + # early wrapping of a wide char. + # This needs to be fixed once we support graphemes taking more than 2 cells. + _restrict_cursor() + + +func tab(): + if _buffer_service.buffer.x >= _buffer_service.cols: + return + var original_x = _buffer_service.buffer.x + _buffer_service.buffer.x = _buffer_service.buffer.next_stop() + # TODO A11y + + +func shift_out(): + _charset_service.set_glevel(1) + + +func shift_in(): + _charset_service.get_glevel(0) + + +func _restrict_cursor(max_col: int = _buffer_service.cols - 1) -> void: + var buffer = _buffer_service.buffer + + self._buffer.x = min(max_col, max(0, self._buffer.x)) + if _core_service.dec_private_modes.origin: + self._buffer.y = min(self._buffer.scroll_bottom, max(self._buffer.scroll_top, self._buffer.y)) + else: + self._buffer.y = min(_buffer_service.rows - 1, max(0, self._buffer.y)) + + # _dirty_row_service.mark_dirty(_buffer_service.buffer.y) + + +# Set absolute cursor position. +func _set_cursor(x: int, y: int) -> void: + # _dirty_row_service.mark_dirty(self._buffer.y) + if _core_service.dec_private_modes.origin: + self._buffer.x = x + self._buffer.y = self._buffer.scroll_top + y + else: + self._buffer.x = x + self._buffer.y = y + + +# Set relative cursor position. +func _move_cursor(x: int, y: int) -> void: + # for relative changes we have to make sure we are within 0 .. cols/rows - 1 + # before calculating the new position + _restrict_cursor() + _set_cursor(self._buffer.y + x, self._buffer.y + y) + + +func index(): + print("TODO: index") + +func next_line(): + print("TODO: next_line") + +func tab_set(): + print("TODO: tab_set") + +func insert_chars(params): + print("TODO: insert_chars") + +func scroll_left(params): + print("TODO: scroll_left") + + +func cursor_up(params) -> void: + # stop at scroll_top + var diff_to_top = self._buffer.y - self._buffer.scroll_top + if diff_to_top >= 0: + _move_cursor(0, -min(diff_to_top, params.get_param(0, 1))) + else: + _move_cursor(0, -params.get_param(0, 1)) + + +func scroll_right(params): + print("TODO: scroll_right") + + +func cursor_down(params): + # stop at scroll_bottom + var diff_to_bottom = self._buffer.scroll_bottom - self._buffer.y + if diff_to_bottom >= 0: + _move_cursor(0, min(diff_to_bottom, params[0] if params[0] else 1)) + else: + _move_cursor(0, params.get_param(0, 1)) + + +func cursor_forward(params): + _move_cursor(params.get_param(0, 1), 0) + + +func cursor_backward(params): + _move_cursor(-params.get_param(0, 1), 0) + + +func cursor_next_line(params): + cursor_down(params) + self._buffer.x = 0 + + +func cursor_preceding_line(params): + cursor_up(params) + self._buffer.x = 0 + + +func cursor_char_absolute(params): + _set_cursor(params.get_param(0, 1) - 1, self._buffer.y) + + +func cursor_position(params): + _set_cursor( + # col + (params.get_param(1, 1)) - 1 if params.size() >= 2 else 0, + # row + (params.get_param(0, 1)) - 1 + ) + + +func char_pos_absolute(params) -> void: + _set_cursor((params[0] if params[0] else 1) - 1, self._buffer.y) + + +func h_position_relative(params): + _move_cursor(params[0] if params[0] else 1, 0) + + +func line_pos_absolute(params): + _set_cursor(self._buffer.x, params.get_param(0, 1) - 1) + + +func v_position_relative(params): + _move_cursor(0, params[0] if params[0] else 1) + + +func h_v_position(params): + cursor_position(params) + +# CSI Ps g Tab Clear (TBC). +# Ps = 0 -> Clear Current Column (default). +# Ps = 3 -> Clear All. +# Potentially: +# Ps = 2 -> Clear Stops on Line. +# http://vt100.net/annarbor/aaa-ug/section6.html +# +# @vt: #Y CSI TBC "Tab Clear" "CSI Ps g" "Clear tab stops at current position (0) or all (3) (default=0)." +# Clearing tabstops off the active row (Ps = 2, VT100) is currently not supported. +func tab_clear(params) -> void: + match params[0]: + 3: + self._buffer.tabs = {} + 0, _: + self._buffer.tabs.erase(self._buffer.x) + + +# CSI Ps I +# Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). +# +# @vt: #Y CSI CHT "Cursor Horizontal Tabulation" "CSI Ps I" "Move cursor `Ps` times tabs forward (default=1)." +func cursor_forward_tab(params) -> void: + if self._buffer.x >= self._buffer.cols: + return + var param = params[0] if params[0] else 1 + while param: + self._buffer.x = self._buffer.next_stop() + param -= 1 + + +func cursor_backward_tab(params) -> void: + if self._buffer.x >= _buffer_service.cols: + return + var param = params[0] if params[0] else 1 + while param: + self._buffer.x = self._buffer.buffer.prev_stop() + param -= 1 + + +# Helper method to erase cells in a terminal row. +# The cell gets replaced with the eraseChar of the terminal. +# params: +# - `y` row index +# - `start` first cell index to be erased +# - `end` end - 1 is last erased cell +func _erase_in_buffer_line(y: int, start: int, end: int, clear_wrap: bool = false) -> void: + var line = self._buffer.lines.get_el(self._buffer.ybase + y) + line.replace_cells(start, end, self._buffer.get_null_cell(_erase_attr_data()), + _erase_attr_data()) + if clear_wrap: + line.is_wrapped = false + + +# Helper method to reset cells in a terminal row. +# The cell gets replaced with the eraseChar of the terminal and the isWrapped property is set to false. +# `y` is the row index +func _reset_buffer_line(y: int) -> void: + var line = self._buffer.lines.get_el(self._buffer.ybase + y) + line.fill(self._buffer.get_null_cell(_erase_attr_data())) + line.is_wrapped = false + + +func erase_in_display(params) -> void: + _restrict_cursor() + var j + match params.get_param(0): + 0: + j = self._buffer.y + # _dirty_row_service.mark_dirty(j) + _erase_in_buffer_line(j, self._buffer.x, _buffer_service.cols, self._buffer.x == 0) + j += 1 + while j < _buffer_service.rows: + _reset_buffer_line(j) + j += 1 + # _dirty_row_service.mark_dirty(j) + 1: + j = self._buffer.y + # _dirty_row_service.mark_dirty(j) + # Deleted front part of line and everything before. This line will no longer be wrapped. + _erase_in_buffer_line(j, 0, self._buffer.x + 1, true) + if self._buffer.x + 1 >= _buffer_service.cols: + # Deleted entire previous line. This next line can no longer be wrapped. + self._buffer.lines.get_el(j + 1).is_wrapped = false + j -= 1 + while j >= 0: + _reset_buffer_line(j) + j -= 1 + # _dirty_row_service.mark_dirty(0) + 2: + j = _buffer_service.rows + # _dirty_row_service.mark_dirty(j - 1) + while j: + _reset_buffer_line(j) + j -= 1 + # _dirty_row_sevice.mark_dirty(0) + 3: + # Clear scrollback (everything not in viewport) + var scrollback_size = self._buffer.lines.length - _buffer_service.rows + if scrollback_size > 0: + self._buffer.lines.trim_start(scrollback_size) + self._buffer.ybase = max(self._buffer.ybase - scrollback_size, 0) + self._buffer.ydisp = max(self._buffer.ydisp - scrollback_size, 0) + # Force a scroll to refresh viewport + emit_signal("scroll_requested", 0) + + +func erase_in_line(params): + _restrict_cursor() + match params.get_param(0): + 0: + _erase_in_buffer_line(buffer.y, buffer.x, _buffer_service.cols) + 1: + _erase_in_buffer_line(buffer.y, 0, buffer.x + 1) + 2: + _erase_in_buffer_line(buffer.y, 0, _buffer_service.cols) + + +func insert_lines(params): + print("TODO: insert_lines") +func delete_lines(params): + print("TODO: delete_lines") + + +func delete_chars(params) -> void: + _restrict_cursor() + var line = buffer.lines.get_el(buffer.ybase + buffer.y) + if line: + line.delete_cells(buffer.x, params.get_param(0, 1), + buffer.get_null_cell(_erase_attr_data()), _erase_attr_data()) + #_dirty_row_service.markDirty(buffer.y) + + +func scroll_up(params): + print("TODO: scroll_up") +func scroll_down(params): + print("TODO: scroll_down") + + +func erase_chars(params) -> void: + _restrict_cursor() + var line = buffer.lines.get_el(buffer.ybase + buffer.y) + if line: + line.replace_cells(buffer.x, buffer.x + params.get_param(0, 1), + buffer.get_null_cell(_erase_attr_data()), _erase_attr_data()) + #this._dirtyRowService.markDirty(this._bufferService.buffer.y) + + +func repeat_preceding_character(params) -> void: + if not _parser.preceding_codepoint: + return + # call print to insert the chars and handle correct wrapping + var length = params.get_param(0, 1) + var data = [] + for _i in range(length): + data.append(_parser.preceding_codepoint) + self.print(data, 0, length) + + +func send_device_attributes_primary(params): + print("TODO: send dev attr primary") +func send_device_attributes_secondary(params): + print("TODO: send dev attr second") + + + + +func set_mode(params): + print("TODO: set mode") + +func reset_mode(params) -> void: + for param in params.params: + match param: + 4: + _core_service.modes.insert_mode = false + 20: + #this._t.convertEol = false + pass + + +func char_attributes(params): + # Optimize a single SGR0 + if params.size() == 1 and params[0] == 0: + _cur_attr_data.fg = DEFAULT_ATTRIBUTE_DATA.fg + _cur_attr_data.bg = DEFAULT_ATTRIBUTE_DATA.bg + return + + var attr = _cur_attr_data + + for p in params.to_array(): + if p >= 30 and p <= 37: + # fg color 8 + attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK) + attr.fg |= Attributes.CM_P16 | (p - 30) + elif p >= 40 and p <= 47: + # bg color 8 + attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK) + attr.bg |= Attributes.CM_P16 | (p - 40) + elif p >= 90 and p <= 97: + # fg color 16 + attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK) + attr.fg |= Attributes.CM_P16 | (p - 90) | 8 + elif p >= 100 and p <= 107: + # bg color 16 + attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK) + attr.bg |= Attributes.CM_P16 | (p - 100) | 8 + elif p == 0: + # default + attr.fg = DEFAULT_ATTRIBUTE_DATA.fg + attr.bg = DEFAULT_ATTRIBUTE_DATA.bg + elif p == 1: + # bold text + attr.fg |= FgFlags.BOLD + elif p == 3: + # italic text + attr.bg |= BgFlags.ITALIC + elif p == 4: + # underlined text + attr.fg |= FgFlags.UNDERLINE + _process_underline(p.get_sub_params()[0] if p.has_sub_params() else UnderlineStyle.SINGLE, attr) + + +func device_status(params): + print("TODO: dev stat") +func device_status_private(params): + print("TODO: dev stat priv") +func soft_reset(params): + print("TODO: soft reset") +func set_cursor_style(params): + print("TODO: set cur styl") + + +func set_scroll_region(params) -> void: + var top = params.get_param(0, 1) + var bottom = params.get_param(1, 0) + + if bottom > _buffer_service.rows or bottom == 0: + bottom = _buffer_service.rows + + if bottom > top: + buffer.scroll_top = top - 1 + buffer.scroll_bottom = bottom - 1 + _set_cursor(0, 0) + + +func save_cursor(params = null): + self._buffer.saved_x = self._buffer.x + self._buffer.saved_y = self._buffer.ybase + self._buffer.y + self._buffer.saved_cur_attr_data.fg = _cur_attr_data.fg + self._buffer.saved_cur_attr_data.bg = _cur_attr_data.bg + self._buffer.saved_charset = _charset_service.charset + + +func window_options(params): + var second = params.get_param(1, 0) + match params.get_param(0): + 14: + pass + 16: + pass + 18: + pass + 22: + pass + 23: + pass + + + +func insert_columns(params): + print("TODO: insert_columns") +func delete_columns(params): + print("TODO: delete_cols") + + +func set_mode_private(params) -> void: + for param in params.params: + match param: + 1: + _core_service.dec_private_modes.application_cursor_keys = true + 2: + _charset_service.set_gcharset(0, Charsets.DEFAULT_CHARSET) + _charset_service.set_gcharset(1, Charsets.DEFAULT_CHARSET) + _charset_service.set_gcharset(2, Charsets.DEFAULT_CHARSET) + _charset_service.set_gcharset(3, Charsets.DEFAULT_CHARSET) + # set VT100 mode here + 3: + # DECCOLM - 132 column mode. + # This is only active if 'set_win_lines' (24) is enabled + # through `options.window_options`. + if _options_service.options.window_options.set_win_lines: + _buffer_service.resize(132, _buffer_service.rows) + emit_signal("reset_requested") + 6: + _core_service.dec_private_modes.origin = true + _set_cursor(0, 0) + 7: + _core_service.dec_private_modes.wraparound = true + 12: + # cursor_blink = true + # TODO handle cursor blink + pass + 45: + _core_service.dec_private_modes.reverse_wraparound = true + 66: + _core_service.dec_private_modes.application_keypad = true + emit_signal("scrollbar_sync_requested") + 9: # X10 Mouse + # no release, no motion, no wheel, no modifiers. + # _core_mouse_service.active_protocal = 'X10' + # TODO + pass + 1000: # vt200 mouse + pass + 1002: # button event mouse + pass + 1003: # any event mouse + pass + 1004: # send focusin/focusout events + # focusin: ^[[I + # focusout: ^[[O + _core_service.dec_private_modes.send_focus = true + 1005: # utf8 ext mode mouse - removed in # 2507 + pass + 1006: # sgr ext mode mouse + pass + 1015: + pass + 25: # show cursor + _core_service.is_cursor_hidden = false + 1048: # alt screen cursor + save_cursor() + 1049: # alt screen buffer cursor + save_cursor() + continue + 47, 1047, 1049: # alt screen buffer + _buffer_service.buffers.activate_alt_buffer(_erase_attr_data()) + _core_service.is_cursor_initialized = true + emit_signal("refresh_rows_requested", 0, _buffer_service.rows - 1) + emit_signal("scrollbar_sync_requested") + 2004: # bracketed paste mode (https://cirw.in/blog/bracketed-paste) + _core_service.dec_private_modes.bracketed_paste_mode = true + + + +func reset_mode_private(params): + for param in params.to_array(): + match param: + 1: + _core_service.dec_private_modes.application_cursor_keys = false + 3: + # DECCOLM - 80 column mode. + # This is only active if 'set_win_lines' (24) is enabled + # through `options.windows_options`. + if _options_service.options.window_options.get("set_win_lines", false): + _buffer_service.resize(80, _buffer_service.rows) + emit_signal("reset_requested") + 6: + _core_service.dec_private_modes.origin = false + _set_cursor(0, 0) + 7: + _core_service.dec_private_modes.wraparound = false + 12: + # cursor_blink = false + # TODO: Handle cursor_blink + pass + 45: + _core_service.dec_private_modes.reverse_wraparound = false + 66: + _core_service.dec_private_modes.application_keypad = false + emit_signal("scrollbar_sync_requested") + 9, 1000, 1002, 1003: + # X10 Mouse, vt200 mouse, button event mouse and any event mouse respectively. + # TODO: Core mouse service + # _core_mouse_service.active_protocal = "NONE" + pass + 1004: # send focusin/focusout events + _core_service.dec_private_modes.send_focus = false + 1005: # utf8 ext mode mouse - removed in #2507 + pass + 1006: # sgr ext mode mouse + # TODO + pass + 1015: # urxvt ext mode mouse - removed in #2507 + pass + 25: # hide cursor + _core_service.is_cursor_hidden = true + pass + 1048: # alt screen cursor + restore_cursor() + 1049, 47, 1047: + # Ensure the selection manager has the correct buffer. + _buffer_service.buffers.activate_normal_buffer() + if param == 1049: + restore_cursor() + _core_service.is_cursor_initialized = true + emit_signal("refresh_rows_requested", 0, _buffer_service.rows - 1) + emit_signal("scrollbar_sync_requested") + 2004: # bracketed paste mode (https://cirw.in/blog/bracketed-paste) + _core_service.dec_private_modes.bracketed_paste_mode = false + + +# Helper to write color information packed with color mode. +func _update_attr_color(color: int, mode: int, c1: int, c2: int, c3: int) -> int: + if mode == 2: + color |= Attributes.CM_RGB + color &= ~Attributes.RGB_MASK + color |= AttributeData.from_color_rgb([c1, c2, c3]) + elif mode == 5: + color &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK) + color |= Attributes.CM_P256 | (c1 & 0xff) + return color + + +# Helper to extract and apply color params/subparams. +# Returns advance for params index. +func _extract_color(params: Array, pos: int, attr) -> int: + # normalize params + # meaning: [target, CM, ign, val, val, val] + # RGB : [ 38/34, 2, ign, r, g, b] + # P256 : [ 38/34, 5, ign, v, ign, ign] + var accu = [0, 0, -1, 0, 0, 0] + + # alignment placeholder for non color space sequences + var c_space = 0 + + # return advance we took in params + var advance = -1 + + while advance + pos < params.size() and advance + c_space < accu.size(): + accu[advance + c_space] = params[pos + advance] + advance += 1 + # TODO FIX and FINISH me + return advance + + +func restore_cursor(params = null) -> void: + self._buffer.x = self._buffer.saved_x if self._buffer.saved_x else 0 + self._buffer.y = max(self._buffer.saved_y - self._buffer.ybase, 0) + _cur_attr_data.fg = self._buffer.saved_cur_attr_data.fg + _cur_attr_data.bg = self._buffer.saved_cur_attr_data.bg + # FIXME _charset_service.charset = _saved_charset + if self._buffer.saved_charset: + _charset_service.charset = self._buffer.saved_charset + _restrict_cursor() + + +# ESC M +# C1.RI +# DEC mnemonic: HTS +# Moves the cursor up one line in the same column. If the cursor is at the top margin, +# the page scrolls down. +# +# @vt: #Y ESC IR "Reverse Index" "ESC M" "Move the cursor one line up scrolling if needed." +# +func reverse_index() -> void: + _restrict_cursor() +# if self._buffer.y == self._buffer.scroll_top: +# # possibly move the code below to term.reverse_scroll() +# # test: echo -ne '\e[1;1H\e[44m\eM\e[0m' +# # blankLine(true) is xterm/linux behavior +# var scroll_region_height = self._buffer.scroll_bottom - self._buffer.scroll_top +# self._buffer.lines.shiftElements(buffer.ybase + buffer.y, scrollRegionHeight, 1); +# buffer.lines.set(buffer.ybase + buffer.y, buffer.getBlankLine(this._eraseAttrData())); +# this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); +# else +# self._buffer.y -= 1 +# _restrict_cursor() # quickfix to not run out of bounds + + +# ESC c +# DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) +# Reset to initial state. +func full_reset() -> void: + _parser.reset() + emit_signal("reset_requested") + + +func reset() -> void: + _cur_attr_data = DEFAULT_ATTRIBUTE_DATA + _erase_attr_data_internal = DEFAULT_ATTRIBUTE_DATA + + +func _process_underline(style: int, attr) -> void: + # treat extended attrs as immutable, thus always clone from old one + # this is needed since the buffer only holds references to it + attr.extended = attr.extended.duplicate() # BEWARE. Maybe don't do this! + + # default to 1 == single underline + if not ~style or style > 5: + style = 1 + attr.extended.underline_style = style + attr.fg |= FgFlags.UNDERLINE + + # 0 deactivates underline + if style == 0: + attr.fg &= ~FgFlags.UNDERLINE + + # update HAS_EXTENDED in BG + attr.update_extended() + + + +# back_color_erase feature for xterm. +func _erase_attr_data(): + _erase_attr_data_internal.bg &= ~(Attributes.CM_MASK | 0xFFFFFF) + _erase_attr_data_internal.bg |= _cur_attr_data.bg & ~0xFC000000 + return _erase_attr_data_internal diff --git a/addons/godot_xterm/parser/escape_sequence_parser.gd b/addons/godot_xterm/parser/escape_sequence_parser.gd index b4bdd5f..e99bb91 100644 --- a/addons/godot_xterm/parser/escape_sequence_parser.gd +++ b/addons/godot_xterm/parser/escape_sequence_parser.gd @@ -244,7 +244,7 @@ func parse(data: Array, length: int): handlers.invert() for handler in handlers: # 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 handlers.invert() if handlers.empty(): diff --git a/addons/godot_xterm/parser/params.gd b/addons/godot_xterm/parser/params.gd index 955b5af..0059d67 100644 --- a/addons/godot_xterm/parser/params.gd +++ b/addons/godot_xterm/parser/params.gd @@ -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_idx.resize(max_length) + +# Gets param at `index` from param if it exists and is non-zero. +# Otherwise returns `default` (which is zero anyway due to zero default +# mode (ZDM), but allows the caller to specify a non-zero default value). +func get_param(index: int, default = 0) -> int: + if index < params.size() and params[index]: + return params[index] + else: + return default + + func add_param(value: int): digit_is_sub = false if length >= _max_length: @@ -78,7 +89,6 @@ func add_sub_param(value: int): sub_params_idx[length - 1] += 1 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 if _reject_digits or (not _length) or (digit_is_sub and _reject_sub_digits): return @@ -86,6 +96,11 @@ func add_digit(value: int): var cur = store[_length - 1] store[_length - 1] = min(cur * 10 + value, MAX_VALUE) if ~cur else value + +func size(): + return params.size() + + func to_array(): var res = [] for i in range(length): diff --git a/addons/godot_xterm/parser/transition_table.gd b/addons/godot_xterm/parser/transition_table.gd index a70628c..860c452 100644 --- a/addons/godot_xterm/parser/transition_table.gd +++ b/addons/godot_xterm/parser/transition_table.gd @@ -1,5 +1,5 @@ -# Copyright (c) 2020 The GodotXterm authors. # Copyright (c) 2019 The xterm.js authors. All rights reserved. +# Ported to GDScript by the GodotXterm authors. # License MIT extends Reference diff --git a/addons/godot_xterm/plugin.gd b/addons/godot_xterm/plugin.gd index 4b7d758..173102b 100644 --- a/addons/godot_xterm/plugin.gd +++ b/addons/godot_xterm/plugin.gd @@ -1,3 +1,5 @@ +# Copyright (c) 2020 The GodotXterm authors. All rights reserved. +# License MIT tool extends EditorPlugin diff --git a/addons/godot_xterm/renderer/base_render_layer.gd b/addons/godot_xterm/renderer/base_render_layer.gd new file mode 100644 index 0000000..8b819b1 --- /dev/null +++ b/addons/godot_xterm/renderer/base_render_layer.gd @@ -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 + + + + + + + + + diff --git a/addons/godot_xterm/renderer/canvas_rendering_context_2d.gd b/addons/godot_xterm/renderer/canvas_rendering_context_2d.gd new file mode 100644 index 0000000..3e14e6e --- /dev/null +++ b/addons/godot_xterm/renderer/canvas_rendering_context_2d.gd @@ -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 diff --git a/addons/godot_xterm/renderer/character_joiner_registry.gd b/addons/godot_xterm/renderer/character_joiner_registry.gd new file mode 100644 index 0000000..c6d86a7 --- /dev/null +++ b/addons/godot_xterm/renderer/character_joiner_registry.gd @@ -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 diff --git a/addons/godot_xterm/renderer/renderer.gd b/addons/godot_xterm/renderer/renderer.gd new file mode 100644 index 0000000..b342b02 --- /dev/null +++ b/addons/godot_xterm/renderer/renderer.gd @@ -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) + + + + + + + + + + diff --git a/addons/godot_xterm/renderer/text_render_layer.gd b/addons/godot_xterm/renderer/text_render_layer.gd new file mode 100644 index 0000000..e73c540 --- /dev/null +++ b/addons/godot_xterm/renderer/text_render_layer.gd @@ -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 diff --git a/addons/godot_xterm/services/buffer_service.gd b/addons/godot_xterm/services/buffer_service.gd new file mode 100644 index 0000000..c6b2498 --- /dev/null +++ b/addons/godot_xterm/services/buffer_service.gd @@ -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) diff --git a/addons/godot_xterm/services/charset_service.gd b/addons/godot_xterm/services/charset_service.gd new file mode 100644 index 0000000..8e098f0 --- /dev/null +++ b/addons/godot_xterm/services/charset_service.gd @@ -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 diff --git a/addons/godot_xterm/services/core_service.gd b/addons/godot_xterm/services/core_service.gd new file mode 100644 index 0000000..9d7b0df --- /dev/null +++ b/addons/godot_xterm/services/core_service.gd @@ -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() diff --git a/addons/godot_xterm/services/options_service.gd b/addons/godot_xterm/services/options_service.gd new file mode 100644 index 0000000..e82c942 --- /dev/null +++ b/addons/godot_xterm/services/options_service.gd @@ -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 diff --git a/addons/godot_xterm/terminal.gd b/addons/godot_xterm/terminal.gd index cbd62c6..36ce5b3 100644 --- a/addons/godot_xterm/terminal.gd +++ b/addons/godot_xterm/terminal.gd @@ -1,16 +1,42 @@ # 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 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 Constants = preload("res://addons/godot_xterm/parser/constants.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 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 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") @@ -26,342 +52,200 @@ const LEFT_BRACKET = 91 const ENTER = 10 const BACKSPACE_ALT = 127 -export (Font) var normal_font = SourceCodeProRegular setget _set_normal_font -export (Font) var bold_font = SourceCodeProBold setget _set_bold_font -export (Font) var italic_font = SourceCodeProItalic setget _set_italics_font -export (Font) var bold_italic_font = SourceCodeProBoldItalic setget _set_bold_italics_font -var buffer -var alternate_buffer -var parser +# TODO: Move me somewhere else. +enum BellStyle { + NONE +} + +signal output(data) + +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 cols = 80 -var rows = 24 -var cell: Vector2 - -# font flags -export(int, FLAGS, - "Bold", - "Italic", # Not xterm-256color - "Underlined", - "Blink", - "Inverse", - "Invisible", - "Strikethrough" # Not xterm-256color - ) var font_flags = Const.FONT_NORMAL - - -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 - +var parser +var _buffer_service +var _core_service +var _charset_service +var _input_handler +var _render_service +var _color_manager +var _scaled_char_width +var _scaled_char_height +var _scaled_cell_width +var _scaled_cell_height +var _scaled_char_top +var _scaled_char_left +var _work_cell = CellData.new() func _ready(): - _calculate_cell_size() - var rect = get_rect() - var rs = rect_size - cols = (rect_size.x / cell.x) as int - rows = (rect_size.y / cell.y) as int + var options = OptionsService.TerminalOptions.new() + options.cols = cols + options.rows = rows + options.font_family = font_family + 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) - alternate_buffer = Buffer.new(rows, cols, true) + _buffer_service = BufferService.new(options_service) + _core_service = CoreService.new() + _charset_service = CharsetService.new() - parser = Parser.new() - # Print handler - parser.set_print_handler(buffer, "insert_at_cursor") + # Register input handler and connect signals. + _input_handler = InputHandler.new(_buffer_service, _core_service, _charset_service, options_service) + _input_handler.connect("bell_requested", self, "bell") + _input_handler.connect("refresh_rows_requested", self, "_refresh_rows") + _input_handler.connect("reset_requested", self, "reset") + _input_handler.connect("scroll_requested", self, "scroll") + _input_handler.connect("windows_options_report_requested", self, "report_windows_options") - # Execute handlers - parser.set_execute_handler(C0.BEL, self, 'bell') - parser.set_execute_handler(C0.LF, buffer, 'line_feed') - 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') + _color_manager = ColorManager.new() + _color_manager.set_theme(colors) + _render_service = Renderer.new(_color_manager.colors, self, _buffer_service, options_service) + + connect("resized", self, "_update_dimensions") + _update_dimensions() - # CSI handlers - parser.set_csi_handler({'final': '@'}, self, 'insert_chars') - 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 _refresh_rows(start_row = 0, end_row = 0): + # Not optimized, just draw + update() -func line_feed(): - pass - -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): if event is InputEventKey and event.pressed: + var data = PoolByteArray([]) accept_event() # TODO: Handle more of these. if (event.control and event.scancode == KEY_C): - send_data(PoolByteArray([3])) + data.append(3) elif event.unicode: - send_data(PoolByteArray([event.unicode])) + data.append(event.unicode) elif event.scancode == KEY_ENTER: - send_data(PoolByteArray([ENTER])) + data.append(ENTER) elif event.scancode == KEY_BACKSPACE: - send_data(PoolByteArray([BACKSPACE_ALT])) + data.append(BACKSPACE_ALT) elif event.scancode == KEY_ESCAPE: - send_data(PoolByteArray([27])) + data.append(27) elif event.scancode == KEY_TAB: - send_data(PoolByteArray([9])) + data.append(9) elif OS.get_scancode_string(event.scancode) == "Shift": pass elif OS.get_scancode_string(event.scancode) == "Control": pass 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): - emit_signal("data_sent", data) +func write(data, callback_target = null, callback_method: String = ''): + _input_handler.parse(data) + if callback_target and callback_method: + callback_target.call(callback_method) + + +func refresh(start = null, end = null) -> void: + pass + + +# Recalculates the character and canvas dimensions. +func _update_dimensions(): + var char_width = 0 + var char_height = 0 + + for font in options_service.options.font_family.values(): + var size = font.get_string_size("W") + char_width = max(char_width, size.x) + char_height = max(char_height, size.y) + + _scaled_char_width = char_width + _scaled_char_height = char_height + + # Calculate the scaled cell height, if line_height is not 1 then the value + # will be floored because since line_height can never be lower then 1, there + # is a guarantee that the scaled line height will always be larger than + # scaled char height. + _scaled_cell_height = floor(_scaled_char_height * options_service.options.line_height) + + # Calculate the y coordinate within a cell that text should draw from in + # order to draw in the center of a cell. + _scaled_char_top = 0 if options_service.options.line_height == 1 else \ + round((_scaled_cell_height - _scaled_char_height) / 2) + + # Calculate the scaled cell width, taking the letter_spacing into account. + _scaled_cell_width = _scaled_char_width + round(options_service.options.letter_spacing) + + # Calculate the x coordinate with a cell that text should draw from in + # order to draw in the center of a cell. + _scaled_char_left = floor(options_service.options.letter_spacing / 2) func _draw(): - # Draw the terminal background - draw_rect(get_rect(), Color(0.0, 0.5, 0.0)) - - # Naive method. Draw the entire buffer starting with row 0. - for row in range(buffer.rows.size()): - #print("Doing the thing for row: ", row) - # Draw each CharacterData. - for col in range(buffer.rows[row].size()): - var data = buffer.rows[row][col] - #print("row: ", ((row + 1) * charHeight), " col: ", (col * charWidth)) - _draw_character(col, row, data) - - # Draw the cursor. - _draw_cursor() - - -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() + # Draw the background and foreground + var buffer = _buffer_service.buffer + for y in range(buffer.ybase, rows): + var line = buffer.lines.get_el(y) + for x in line.length: + line.load_cell(x, _work_cell) + draw_rect(Rect2(x * _scaled_cell_width, y * _scaled_cell_height, + (cols - x) * _scaled_cell_width, 1 * _scaled_cell_height), Color()) + var color = _color_manager.colors.ansi[_work_cell.get_fg_color()] if _work_cell.get_fg_color() >= 0 else Color(1, 1, 1) + draw_char(options_service.options.font_family.regular, + Vector2(x * _scaled_cell_width + _scaled_char_left, + y * _scaled_cell_height + _scaled_char_top + _scaled_char_height / 2), + _work_cell.get_chars() if _work_cell.get_chars() else ' ', "", color) + # Draw the cursor + # Draw selection diff --git a/project.godot b/project.godot index a2b366e..8d57f74 100644 --- a/project.godot +++ b/project.godot @@ -8,11 +8,28 @@ 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={ - +"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] config/name="GodotXterm" diff --git a/scenes/demo.gd b/scenes/demo.gd index 0d703c5..ec2e13e 100644 --- a/scenes/demo.gd +++ b/scenes/demo.gd @@ -5,15 +5,20 @@ extends Control signal data_received(data) - # The user must have these programs installed for this to work. const dependencies = PoolStringArray(['which', 'socat', 'bash']) 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 stream_peer = StreamPeerTCP.new() +var record_file func _ready(): @@ -22,11 +27,11 @@ func _ready(): if exit_code != 0: OS.alert("Make sure the following programs are installed and in your $PATH: " + \ dependencies.join(", ") + ".", "Misssing Dependencies!") - - # Start socat. - socat_pid = OS.execute("socat", - ["-d", "-d", "tcp-l:%d,bind=%s,reuseaddr,fork" % [port, host], - "exec:bash,pty,setsid,stderr,login,ctty"], false) + else: + # Start socat. + socat_pid = OS.execute("socat", + ["-d", "-d", "tcp-l:%d,bind=%s,reuseaddr,fork" % [port, host], + "exec:bash,pty,setsid,stderr,login,ctty"], false) # Create a StreamPeerTCP to connect to socat. var err = stream_peer.connect_to_host(host, port) @@ -37,12 +42,21 @@ func _ready(): var status = stream_peer.get_status() 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. - $Terminal.connect('data_sent', self, 'send_data') - connect("data_received", $Terminal, "receive_data") + $Terminal.connect('output', self, 'send_data') + connect("data_received", $Terminal, "write") func send_data(data: PoolByteArray): + if record: + # Save the data and timestamp to a file + record_file.write() stream_peer.put_data(data) @@ -59,5 +73,7 @@ func _process(delta): func _exit_tree(): + if record: + record_file.close() if socat_pid != -1: OS.execute("kill", ["-9", socat_pid], false) diff --git a/scenes/demo.tscn b/scenes/demo.tscn index 2df8e4e..87f02e0 100644 --- a/scenes/demo.tscn +++ b/scenes/demo.tscn @@ -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://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 -margin_left = 120.0 -margin_top = 80.0 -margin_right = 120.0 -margin_bottom = 80.0 script = ExtResource( 1 ) __meta__ = { "_edit_use_anchors_": false } [node name="Terminal" type="Control" parent="."] -margin_right = 631.0 -margin_bottom = 401.0 +margin_left = 95.937 +margin_top = 44.6138 +margin_right = 695.937 +margin_bottom = 444.614 +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 ) +} +font_size = 16 +colors = { +"black": Color( 0.121569, 0.00784314, 0.00784314, 1 ) +} +window_options = { + +} diff --git a/scenes/showcase.tscn b/scenes/showcase.tscn new file mode 100644 index 0000000..14aa48e --- /dev/null +++ b/scenes/showcase.tscn @@ -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 = { + +} diff --git a/test/integration/test_terminal.gd b/test/integration/test_terminal.gd index 5619251..99b2309 100644 --- a/test/integration/test_terminal.gd +++ b/test/integration/test_terminal.gd @@ -25,7 +25,7 @@ class TestBuffer: func handle_csi(params): - calls.append(['csi', params]) + calls.append(['csi', params.to_array()]) func clear(): @@ -68,7 +68,7 @@ func test_prints_printables(): func skip_test_c0(): for code in C0.values(): 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: assert_eq(buffer.calls, []) else: @@ -81,7 +81,7 @@ func skip_test_c0(): func skip_test_c1(): for code in C1.values(): 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.printed, '') parser.reset() diff --git a/test/test.tscn b/test/test.tscn index 1214701..fe35273 100644 --- a/test/test.tscn +++ b/test/test.tscn @@ -12,6 +12,6 @@ __meta__ = { "_edit_use_anchors_": false } _yield_between_tests = false -_directory1 = "res://test/unit" -_directory2 = "res://test/integration" +_include_subdirectories = true +_directory1 = "res://test" _double_strategy = 1 diff --git a/test/test_utils.gd b/test/test_utils.gd new file mode 100644 index 0000000..a43614c --- /dev/null +++ b/test/test_utils.gd @@ -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 diff --git a/test/unit/.test_input_handler.gd.swp b/test/unit/.test_input_handler.gd.swp new file mode 100644 index 0000000000000000000000000000000000000000..edf75473cd0f53742c267ffd8039046644c05ece GIT binary patch literal 16384 zcmeI3TWlOx8OKjcOR}^Hfq>|R(vw|~cNJ&6ySD4#B!wo>5P_8lCnd;ohuPh;_9Ww( zWzN~y=28R#B&eFEm8f_?0xB*=sss;-P#;Qo;1x?ETMgev-X?fH6n--Xfh} z<)db8_jt={tWu0Uper0|`Q9;6S8`~^Yj|qMf)f7RkP=c2wOw23U#9K0+RB`@+y?G3 zW>&3wwWhP{nFOwF3A9yn$IeY`xLPTbAPV_yeCr#JT-!3qMxRL_lRzecOahq%G6`f7 z$Rv`nQ{W&N10`@LcpJC_ zT)2s`r@;5Ym%)B82JQl*U>o?$>lphQSOE8d7jumL6Z{sO2fqPlz!$+Jcq{nhM#i28 z=fIQTB=|768T@$zV}Am_1m6RXf@!b?TzW0q1LwhM@EEYbo4^YL1T*mYslJx7)X*;OrLNwls^@zEl^`eD{RR|!`YlRX>c z%5RI(AV1`J&ZP2fcjg}7w-$?2EH_;$#d^&99Mwp<#JBTE7Cr9bJ2=W+Wr3{M1obJ{ zrsc>Ul1;vMBYaB=yo+NtY}s-k`$Lv*%}Jx_dvm50%tl#HMsn_q;Lup$dmv4g>wa|+ zS4UZ{z+1XjK2@HE^nH6~R&l)e!)wCv=DF?z%VEBgeZx&s6rs848k>j-`aDJX7kuS_~Ii zN_s}8TU1B4^;BJDJ&mono~ldhX{tiY$q`KzTSJM_k7QgliLp?6jjk(mBWpy14^m4KTTGllID1g55 z4d_Wqe!-!k_gG}Y?6|yx0Xg=Zt;jFP>0it1A-@4TsmWbWp;?qh;c!k+dTKftKN(5g zR%%HMhWB(tpq*Qi0M(J$p!!UFLj@<2UWGoF9I8H*TJwy-+m+r4~sDY@wpAjQ<7~5WvOmf;2%IB=1RdQ4jjhO zA^B8Sy>k03HW*@GfvW_!r{*XTW#CgJ1^Kz&pU5;8w5|Yyum=b>QEK`(FZ-A9x0w z244d22bU4|KM1}6YM=^UM7;kjI0HToc7t(nE4Yk!{{nafyc?WHd`~g|6JP<1gWJJH z#P*MaI;a5)7~nQ=1Gt13|EJ(5;K$%;@JT>9fSuqr@E^?61wixl0L|I|`V$KlpzB`Y zQS$a4inH1+DUg5R>^JcT&z64WwIiL}$ASA6mZLII`bmWunQ01J;ZSzFeV|eu9vL0L zttrK7gvW-g`<~;}tom`(0+&411a_YNF;N^HI6CoOEP-@qVWd@&5-S#rpg+jr4^`0V&KYu& zG025#xx6=a+6cQ>DWfnMqRtH{9ho%U*A}&4HasCs7(labwUH^XTqFycHvSinFdffp z1-?=tCl*@x4?igj7)iThi4jR|Gi>=GjIkveB|}G$yI3JLwoU}OLQ;YPDY`Qy{NPZh zCEi)y#i2Xfz9c_OBH6I$A?M&&FpkKHwddTPw^QDtLbboVVUfE+cNa*wO&iJ_<`+L3 zHf05%=q>Um;JQ?)Uo_*8hAtmiOp0rDR7*J$tLB{$L9&8MvH}W{=U8-vg8R6K5AVX) z?z9s`&dV;oeY;&sx*B1O6C~PF!lO(f@@EKnk9oFR9Iyrg8F$+zD^`HOF;)X+L65eg zb3Rq{f6Wzkt*?xlQX_~>f4TSSpy}0@3))NuxhS|q+=?bccUX)1<*k4#y0Ax98Co@4 zx!KAMGwAs|CA$0gH(clBdhWXl+4^3o$#`1+Ra7P>y`B#{)-@+jIaK5%daFND5K ztURtp=`=o7r9A5Vtmg<0=SB$ks$}CSL{Zd|cI7&G+g^$O^wJoq#2@zbxC`S#xmEYT zQaXHHo>%qgOyZ#vt?o4rA$k<@D%u!*3U*!h#%ekCHEoThM+_Zj3 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), '') diff --git a/test/unit/test_text_decoder.gd b/test/unit/input/test_text_decoder.gd similarity index 96% rename from test/unit/test_text_decoder.gd rename to test/unit/input/test_text_decoder.gd index 2a6bbb1..65b5686 100644 --- a/test/unit/test_text_decoder.gd +++ b/test/unit/input/test_text_decoder.gd @@ -16,9 +16,10 @@ const TEST_STRINGS = [ "๋ชจ๋“  ๊ตญ๋ฏผ์€ ํ–‰์œ„์‹œ์˜ ๋ฒ•๋ฅ ์— ์˜ํ•˜์—ฌ ๋ฒ”์ฃ„๋ฅผ ๊ตฌ์„ฑํ•˜์ง€ ์•„๋‹ˆํ•˜๋Š” ํ–‰์œ„๋กœ ์†Œ์ถ”๋˜์ง€ ์•„๋‹ˆํ•˜๋ฉฐ. ์ „์ง๋Œ€ํ†ต๋ น์˜ ์‹ ๋ถ„๊ณผ ์˜ˆ์šฐ์— ๊ด€ํ•˜์—ฌ๋Š” ๋ฒ•๋ฅ ๋กœ ์ •ํ•œ๋‹ค, ๊ตญํšŒ๋Š” ํ—Œ๋ฒ• ๋˜๋Š” ๋ฒ•๋ฅ ์— ํŠน๋ณ„ํ•œ ๊ทœ์ •์ด ์—†๋Š” ํ•œ ์žฌ์ ์˜์› ๊ณผ๋ฐ˜์ˆ˜์˜ ์ถœ์„๊ณผ ์ถœ์„์˜์› ๊ณผ๋ฐ˜์ˆ˜์˜ ์ฐฌ์„ฑ์œผ๋กœ ์˜๊ฒฐํ•œ๋‹ค. ๊ตฐ์ธยท๊ตฐ๋ฌด์›ยท๊ฒฝ์ฐฐ๊ณต๋ฌด์› ๊ธฐํƒ€ ๋ฒ•๋ฅ ์ด ์ •ํ•˜๋Š” ์ž๊ฐ€ ์ „ํˆฌยทํ›ˆ๋ จ๋“ฑ ์ง๋ฌด์ง‘ํ–‰๊ณผ ๊ด€๋ จํ•˜์—ฌ ๋ฐ›์€ ์†ํ•ด์— ๋Œ€ํ•˜์—ฌ๋Š” ๋ฒ•๋ฅ ์ด ์ •ํ•˜๋Š” ๋ณด์ƒ์™ธ์— ๊ตญ๊ฐ€ ๋˜๋Š” ๊ณต๊ณต๋‹จ์ฒด์— ๊ณต๋ฌด์›์˜ ์ง๋ฌด์ƒ ๋ถˆ๋ฒ•ํ–‰์œ„๋กœ ์ธํ•œ ๋ฐฐ์ƒ์€ ์ฒญ๊ตฌํ•  ์ˆ˜ ์—†๋‹ค.", "ูƒุงู† ูุดูƒู‘ู„ ุงู„ุดุฑู‚ูŠ ู…ุน, ูˆุงุญุฏุฉ ู„ู„ู…ุฌู‡ูˆุฏ ุชุฒุงู…ู†ุงู‹ ุจุนุถ ุจู„. ูˆุชู… ุฌู†ูˆุจ ู„ู„ุตูŠู† ุบูŠู†ูŠุง ู„ู…, ุงู† ูˆุจุฏูˆู† ูˆูƒุณุจุช ุงู„ุฃู…ูˆุฑ ุฐู„ูƒ, ุฃุณุฑ ุงู„ุฎุงุณุฑ ุงู„ุงู†ุฌู„ูŠุฒูŠุฉ ู‡ูˆ. ู†ูุณ ู„ุบุฒูˆ ู…ูˆุงู‚ุนู‡ุง ู‡ูˆ. ุงู„ุฌูˆ ุนู„ุงู‚ุฉ ุงู„ุตุนุฏุงุก ุงู†ู‡ ุฃูŠ, ูƒู…ุง ู…ุน ุจู…ุจุงุฑูƒุฉ ู„ู„ุฅุชุญุงุฏ ุงู„ูˆุฒุฑุงุก. ุชุฑุชูŠุจ ุงู„ุฃูˆู„ู‰ ุฃู† ุญุฏู‰, ุงู„ุดุชูˆูŠุฉ ุจุงุณุชุญุฏุงุซ ู…ุฏู† ุจู„, ูƒุงู† ู‚ุฏ ุฃูˆุณุน ุนู…ู„ูŠุฉ. ุงู„ุฃูˆุถุงุน ุจุงู„ู…ุทุงู„ุจุฉ ูƒู„ ู‚ุงู…, ุฏูˆู† ุฅุฐ ุดู…ุงู„ ุงู„ุฑุจูŠุนุŒ. ู‡ูุฒู… ุงู„ุฎุงุตู‘ุฉ ูฃู  ุฃู…ุง, ู…ุงูŠูˆ ุงู„ุตูŠู†ูŠุฉ ู…ุน ู‚ุจู„.", "ืื• ืกื“ืจ ื”ื—ื•ืœ ืžื™ื–ืžื™ ืงืจื™ืžื™ื ื•ืœื•ื’ื™ื”. ืงื”ื™ืœื” ื‘ื’ืจืกื” ืœื•ื™ืงื™ืคื“ื™ื ืืœ ื”ื™ื, ืฉืœ ืฆืขื“ ืฆื™ื•ืจ ื•ืืœืงื˜ืจื•ื ื™ืงื”. ืžื“ืข ืžื” ื‘ืจื™ืช ื”ืžื–ื ื•ืŸ ืืจื›ื™ืื•ืœื•ื’ื™ื”, ืืœ ื˜ื‘ืœืื•ืช ืžื‘ื•ืงืฉื™ื ื›ืœืœ. ืžืืžืจืฉื™ื—ื”ืฆืคื” ื”ืขืจื™ื›ื”ื’ื™ืจืกืื•ืช ืฉื›ืœ ืืœ, ื›ืชื‘ ืขื™ืฆื•ื‘ ืžื•ืฉื’ื™ ืฉืœ. ืงื‘ืœื• ืงืœืืกื™ื™ื ื‘ ืžืชืŸ. ื ื‘ื—ืจื™ื ืื•ื•ื™ืจื•ื ืื•ื˜ื™ืงื” ืื ืžืœื, ืœื•ื— ืœืžื ื•ืข ืืจื›ื™ืื•ืœื•ื’ื™ื” ืžื”. ืืจืฅ ืœืขืจื•ืš ื‘ืงืจื‘ืช ืžื•ื ื—ื•ื ื™ื ืื•, ืขื–ืจื” ืจืงื˜ื•ืช ืœื•ื™ืงื™ืคื“ื™ื ืื—ืจ ื’ื.", - "ะ›ะพั€ะตะผ แƒšแƒแƒ แƒ”แƒ› เค…เคงเคฟเค•เคพเค‚เคถ ่ฆงๅ…ญๅญ ๅ…ซใƒกใƒซ ๋ชจ๋“  ื‘ืงืจื‘ืช ๐Ÿ’ฎ ๐Ÿ˜‚ รคggg 123โ‚ฌ ๐„ž.", + "ะ›ะพั€ะตะผ แƒšแƒแƒ แƒ”แƒ› เค…เคงเคฟเค•เคพเค‚เคถ ่ฆงๅ…ญๅญ ๅ…ซใƒกใƒซ ๋ชจ๋“  ื‘ืงืจื‘ืช รคggg 123โ‚ฌ .", ] + func test_utf32_to_utf8(): # 1 byte utf8 character assert_eq( @@ -45,10 +46,6 @@ func test_utf32_to_utf8(): 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(): assert_eq( @@ -71,6 +68,11 @@ class TestUtf8ToUtf32Decoder: decoder.clear() target.clear() target.resize(5) + + func test_lol(): + var target = [0, 0, 0, 0] + decoder.decode('ํ ฝํฒฉ'.substr(0, 1).to_utf8(), target) + assert_eq(target, []) func test_full_code_point_0_to_65535(): # 1/2/3 byte sequences diff --git a/test/unit/test_dcs_parser.gd b/test/unit/parser/test_dcs_parser.gd similarity index 100% rename from test/unit/test_dcs_parser.gd rename to test/unit/parser/test_dcs_parser.gd diff --git a/test/unit/test_escape_sequence_parser.gd b/test/unit/parser/test_escape_sequence_parser.gd similarity index 93% rename from test/unit/test_escape_sequence_parser.gd rename to test/unit/parser/test_escape_sequence_parser.gd index 9e55a64..c2fc64f 100644 --- a/test/unit/test_escape_sequence_parser.gd +++ b/test/unit/parser/test_escape_sequence_parser.gd @@ -36,7 +36,7 @@ class TestTerminal: func handle_execute(code: int): - var flag = Decoder.string_from_codepoint(code) + var flag = char(code) calls.append(['exe', flag]) @@ -137,14 +137,14 @@ func test_state_GROUND_execute_action(): var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20) for exe in exes: parser.current_state = ParserState.GROUND - parse(parser, Decoder.string_from_codepoint(exe)) + parse(parser, char(exe)) assert_eq(parser.current_state, ParserState.GROUND) parser.reset() func test_state_GROUND_print_action(): var printables = range(0x20, 0x7f) # NOTE: DEL excluded for printable in printables: - var string = Decoder.string_from_codepoint(printable) + var string = char(printable) parser.current_state = ParserState.GROUND parse(parser, string) 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) for exe in exes: parser.current_state = ParserState.ESCAPE - var data = Decoder.string_from_codepoint(exe) + var data = char(exe) parse(parser, data) assert_eq(parser.current_state, ParserState.ESCAPE, '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) for dispatch in dispatches: parser.current_state = ParserState.ESCAPE - var data = Decoder.string_from_codepoint(dispatch) + var data = char(dispatch) parse(parser, data) assert_eq(parser.current_state, ParserState.GROUND, '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) for c in collect: parser.current_state = ParserState.ESCAPE - var data = Decoder.string_from_codepoint(c) + var data = char(c) parse(parser, data) assert_eq(parser.current_state, ParserState.ESCAPE_INTERMEDIATE) 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(): var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20) for exe in exes: - var data = Decoder.string_from_codepoint(exe) + var data = char(exe) parser.current_state = ParserState.ESCAPE_INTERMEDIATE parse(parser, data) 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(): var collect = range(0x20, 0x30) for c in collect: - var data = Decoder.string_from_codepoint(c) + var data = char(c) parser.current_state = ParserState.ESCAPE_INTERMEDIATE parse(parser, data) 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(): var collect = range(0x30, 0x7f) for c in collect: - var data = Decoder.string_from_codepoint(c) + var data = char(c) parser.current_state = ParserState.ESCAPE_INTERMEDIATE parse(parser, data) 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(): var exes = range(0x00, 0x18) + [0x19] + range(0x1c, 0x20) for exe in exes: - var data = Decoder.string_from_codepoint(exe) + var data = char(exe) parser.current_state = ParserState.CSI_ENTRY parse(parser, data) 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(): var dispatches = range(0x40, 0x7f) for dispatch in dispatches: - var data = Decoder.string_from_codepoint(dispatch) + var data = char(dispatch) parser.current_state = ParserState.CSI_ENTRY parse(parser, data) 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'] for param in params: 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.params, [param - 48], 'param: 0x%x' % param) 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(): var exes = range(0x00, 0x018) + [0x19] + range(0x1c, 0x20) for exe in exes: - var data = Decoder.string_from_codepoint(exe) + var data = char(exe) parser.current_state = ParserState.CSI_PARAM parse(parser, data) assert_eq(parser.current_state, ParserState.CSI_PARAM) @@ -375,7 +375,7 @@ func test_state_CSI_PARAM_param_action(): var params = range(0x30, 0x3a) for param in params: 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.params, [param - 48], 'param: 0x%x' % param) parser.reset() @@ -391,7 +391,7 @@ func test_state_CSI_PARAM_ignore(): func test_trans_CSI_PARAM_to_GROUND_with_csi_dispatch_action(): var dispatches = range(0x40, 0x7f) for dispatch in dispatches: - var data = Decoder.string_from_codepoint(dispatch) + var data = char(dispatch) parser.current_state = ParserState.CSI_PARAM parser.params = [0, 1] 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(): for collect in range(0x20, 0x30): - var data = Decoder.string_from_codepoint(collect) + var data = char(collect) parser.current_state = ParserState.CSI_ENTRY parse(parser, data) assert_eq(parser.current_state, ParserState.CSI_INTERMEDIATE) diff --git a/test/unit/parser/test_parser.gd b/test/unit/parser/test_parser.gd new file mode 100644 index 0000000..c3987cc --- /dev/null +++ b/test/unit/parser/test_parser.gd @@ -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]]) + + + + + + + diff --git a/test/unit/renderer/test_canvas_rendering_context_2d.gd b/test/unit/renderer/test_canvas_rendering_context_2d.gd new file mode 100644 index 0000000..b4862a1 --- /dev/null +++ b/test/unit/renderer/test_canvas_rendering_context_2d.gd @@ -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) diff --git a/test/unit/renderer/test_character_joiner_registry.gd b/test/unit/renderer/test_character_joiner_registry.gd new file mode 100644 index 0000000..be21289 --- /dev/null +++ b/test/unit/renderer/test_character_joiner_registry.gd @@ -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), []) diff --git a/test/unit/test_input_handler.gd b/test/unit/test_input_handler.gd new file mode 100644 index 0000000..fff1e21 --- /dev/null +++ b/test/unit/test_input_handler.gd @@ -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(["๏ฟฅ๏ฟฅ", "๏ฟฅ"]))