Add more features, bug fixes and bugs ;-)

Most notably:
- Reflow is now working. Terminal size will fill the window and
cols/rows will be resized/calculated based on window and font size.
- Added support for different fonts (i.e. bold, italic, bolditalic).
- Enabled blinking characters.
- Adde more tests and caught a few subtle bugs.
- Removed renderer code (which was part of xterm.js) and just
doing naive rendering in terminal.gd, but it seems to perform
a lot faster.

Still not working completely:
- vim (some weirdness going on).
- vttest (more weirdness).

Todo:
- Fix the above.
- Draw the cursor!
- Improve performance. Performance is still not great. The terminal
becomes unusable when running `yes` or `cmatrix -r`.
This commit is contained in:
Leroy Hopson 2020-05-19 18:45:18 +07:00
parent 0769592a1b
commit 0d4e10f5ab
30 changed files with 2640 additions and 1157 deletions

Binary file not shown.

View file

@ -1,271 +0,0 @@
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
# License MIT
extends Reference
const CharData = preload("res://addons/godot_xterm/char_data.gd")
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
const MAX_BUFFER_SIZE = 32768 # 32 KiB
# Erase in Line (EL)
enum {EL_RIGHT, EL_LEFT, EL_ALL}
enum {FONT_NORMAL, FONT_BOLD, FONT_BLINK}
# Places a tab stop after every 8 columns.
var tabWidth = 8
var rows = [[]] # array of CharData
var fg = Color(1.0, 1.0, 1.0) # foreground color
var bg = Color(0.0, 0.0, 0.0) # background color
var font # font
var font_flags = FONT_NORMAL
var crow = 0 setget _set_crow # cursor's row
var ccol = 0 setget _set_ccol # cursor's column
var ccol_saved: int
var crow_saved: int
var num_rows = 20
var num_cols = 70
var savedBuffer
var savedCursorRow
var savedCursorCol
func _init(num_rows: int, num_columns: int, alternate: bool = false):
rows = []
rows.resize(num_rows)
for i in range(rows.size()):
var cols = []
cols.resize(num_columns)
for j in range(cols.size()):
cols[j] = CharData.new(" ", bg, fg, font_flags)
rows[i] = cols
func _get_buffer_size():
# Get the size of the (virtual) buffer.
# Count each CharData as one byte even though it might be multiple bytes
# in the case of unicode characters.
var size = 0
for row in rows:
size += row.size()
return size
func _set_rows(rows):
print("rows: ", rows)
func _set_crow(row: int):
print("setting crow")
# Ensure there are enoungh rows in the
# buffer for the new cursor position.
if row >= rows.size():
rows.resize(row + 1)
# resize() uses null for new elements.
# but a row should be an array so we
# need to replace null values.
for i in range(rows.size()):
if rows[i] == null:
rows[i] = []
crow = row
func _set_ccol(col: int):
# Ensure there are enough columns in the
# row for the new cursor position.
print("da size: ", rows[crow].size())
if col >= rows[crow].size():
rows[crow].resize(col + 1)
print("da new size: ", rows[crow].size())
for i in range(rows[crow].size()):
if rows[crow][i] == null:
rows[crow][i] = CharData.new(' ', bg, fg)
ccol = col
func save_cursor():
ccol_saved = ccol
crow_saved = crow
func restore_cursor():
ccol = ccol_saved
crow = crow_saved
func insert_at_cursor(d, start: int = 0, end: int = 1):
var string
if typeof(d) == TYPE_ARRAY:
string = Decoder.utf32_to_string(d.slice(start, end - 1))
else:
string = d
var row = rows[crow]
for i in range(string.length()):
var data = CharData.new(string[i], bg, fg, font_flags)
if ccol < row.size():
row[ccol] = data
else:
row.resize(ccol)
for i in range(row.size()):
if row[i] == null:
row[i] = CharData.new(' ', bg, fg, font_flags)
row.append(data)
# Update the cursor position.
ccol += 1
func insert_tab():
print("Insert a tab!")
# Insert a space.
insert_at_cursor(' ')
# Keep inserting spaces until cursor is at next Tab stop.
while ccol % tabWidth != 0:
insert_at_cursor(' ')
# cr
func carriage_return():
ccol = 0
# lf
func line_feed():
rows.resize(rows.size() + 1)
rows[-1] = []
crow = crow + 1
# bs
# Deletes the element before the current cursor position.
func backspace():
rows[crow].remove(ccol - 1)
ccol = ccol - 1
# cup
# Move the cursor to the given row and column.
# For example cursor_position(0, 0) would move
# the cursor to the top left corner of the terminal.
func cursor_position(params: Array) -> void:
var row = params[0] if params.size() > 0 else 1
var col = params[1] if params.size() > 1 else 1
# Origin is (0,0) so row 1, col 1 would be 0,0.
if col != 0:
self.ccol = col - 1
else:
self.ccol = 0
if row != 0:
self.crow = row - 1
else:
self.crow = 0
# ed 3
func erase_saved_lines():
rows = [[]]
print("saved lines erased")
# el
func erase_in_line(section):
return
match section:
EL_LEFT, EL_ALL:
for i in range(0, ccol):
rows[crow][i] = CharData.new(" ")
print("Erased the thing")
if section == EL_ALL:
continue
EL_RIGHT, _:
for i in range(ccol, rows[crow].size()):
rows[crow][i] = CharData.new(" ")
print("Erased the thing")
# ed 0 (default)
func erase_below():
# Erase from the cursor through to the end of the display.
save_cursor()
while crow < num_rows:
erase_in_line(EL_RIGHT)
_set_ccol(0)
_set_crow(crow + 1)
restore_cursor()
func set_scrolling_region(top: int, bottom: int):
print("set_scrolling_position")
# Not sure what this does yet.
# Make default be full window size.
pass
func set_font(fontState: int, set: bool = true):
match fontState:
FONT_NORMAL:
pass
func set_font_flag(flag: int, set: bool = true):
print("setting font flag!")
if set: # Set the flag
font_flags |= (1 << flag)
else: # Clear the flag
font_flags &= ~(1 << flag)
print("font flag is set!")
print(font_flags)
# Clear all font flags. Returns font to default state.
func reset_font_flags():
font_flags = FONT_NORMAL
# setf
func set_foreground(color: Color = Color(1.0, 1.0, 1.0)):
fg = color
# setb
func set_background(color: Color = Color(0.0, 0.0, 0.0)):
bg = color
# setaf
func set_a_foreground(params):
pass
# setab
func set_a_background(params):
pass
func reset_sgr():
set_foreground()
set_background()
reset_font_flags()
func repeat_preceding_character(times: int = 0):
var preceding_char
if ccol == 0:
preceding_char = rows[crow-1][-1]
else:
preceding_char = rows[crow][ccol-1]
print("Repeating preceding char ", preceding_char.ch, " ", times, " times")
for i in range(times):
insert_at_cursor(preceding_char.ch)
# Save the buffer (useful when switching to the alternate buffer)
func save():
savedBuffer = rows
savedCursorCol = ccol
savedCursorRow = crow
# Full Reset
func reset():
rows = [[]]
crow = 0
ccol = 0
fg = Color(1.0, 1.0, 1.0)
bg = Color(0.0, 0.0, 0.0)

View file

@ -15,6 +15,12 @@ var bg = 0
var extended = ExtendedAttrs.new()
static func to_color_rgb(value: int) -> Color:
# Create color from RGB format.
return Color("%02x%02x%02x" % [value >> Attributes.RED_SHIFT & 255,
value >> Attributes.GREEN_SHIFT & 255, value & 255])
# flags
func is_inverse() -> int:
return fg & FgFlags.INVERSE
@ -27,7 +33,7 @@ func is_blink() -> int:
func is_invisible() -> int:
return fg & FgFlags.INVISIBLE
func is_italic() -> int:
return fg & BgFlags.ITALIC
return bg & BgFlags.ITALIC
func is_dim() -> int:
return fg & BgFlags.DIM
@ -60,13 +66,30 @@ func get_fg_color() -> int:
Attributes.CM_RGB:
return fg & Attributes.RGB_MASK
_:
return -1
return -1 # CM_DEFAULT defaults to -1
func get_bg_color() -> int:
match bg & Attributes.CM_MASK:
Attributes.CM_P16, Attributes.CM_P256:
return bg & Attributes.PCOLOR_MASK
Attributes.CM_RGB:
return bg & Attributes.RGB_MASK
_:
return -1 # CM_DEFAULT defaults to -1
func has_extended_attrs() -> int:
return bg & BgFlags.HAS_EXTENDED
func update_extended() -> void:
if extended.is_empty():
bg &= ~BgFlags.HAS_EXTENDED
else:
bg |= BgFlags.HAS_EXTENDED
func get_underline_color() -> int:
if bg & BgFlags.HAS_EXTENDED and ~extended.underline_color:
match extended.underline_color & Attributes.CM_MASK:
@ -117,6 +140,9 @@ func get_underline_style():
class ExtendedAttrs:
extends Reference
# Extended attributes for a cell.
# Holds information about different underline styles and color.
var underline_style = UnderlineStyle.NONE
@ -125,3 +151,18 @@ class ExtendedAttrs:
func _init():
underline_style
func duplicate():
# Workaround as per: https://github.com/godotengine/godot/issues/19345#issuecomment-471218401
var AttributeData = load("res://addons/godot_xterm/buffer/attribute_data.gd")
var clone = AttributeData.ExtendedAttrs.new()
clone.underline_style = underline_style
clone.underline_color = underline_color
return clone
# Convenient method to indicate whether the object holds no additional information,
# that needs to be persistant in the buffer.
func is_empty():
return underline_style == UnderlineStyle.NONE

View file

@ -10,7 +10,7 @@ const Charsets = preload("res://addons/godot_xterm/data/charsets.gd")
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
const CircularList = preload("res://addons/godot_xterm/circular_list.gd")
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
const BufferReflow = preload("res://addons/godot_xterm/buffer/buffer_reflow.gd")
const MAX_BUFFER_SIZE = 4294967295 # 2^32 - 1
@ -51,6 +51,286 @@ func _init(has_scrollback: bool, options_service, buffer_service):
setup_tab_stops()
# Resizes the buffer, adjusting its data accordingly.
# @param new_cols The new number of columns.
# @param new_rows The new number of rows.
func resize(new_cols: int, new_rows: int) -> void:
# store reference to null cell with default attrs
var null_cell = get_null_cell(AttributeData.new())
# Increase max length if needed before adjustments to allow space to fill
# as required.
var new_max_length = _get_correct_buffer_length(new_rows)
if new_max_length > lines.max_length:
lines.max_length = new_max_length
# The following adjustments should only happen if the buffer has been
# initialized/filled.
if lines.length > 0:
# Deal with columns increasing (reducing needs to happen after reflow)
if _cols < new_cols:
for i in range(lines.length):
lines.get_line(i).resize(new_cols, null_cell)
# Resize rows in both directions as needed
var add_to_y = 0
if _rows < new_rows:
for y in range(_rows, new_rows):
if lines.length < new_rows + ybase:
if _options_service.options.windows_mode:
# Just add the new missing rows on Windows as conpty reprints the screen with it's
# view of the world. Once a line enters scrollback for conpty it remains there
lines.push(BufferLine.new(new_cols, null_cell))
else:
if ybase > 0 and lines.length <= ybase + y + add_to_y + 1:
# There is room above the buffer and there are no empty elements below the line,
# scroll up
ybase -= 1
add_to_y += 1
if ydisp > 0:
# Viewport is at the top of the buffer, must increase downwards
ydisp -= 1
else:
# Add a blank line if tere is no buffer left at the top to srcoll to, or if there
# are blank lines after the cursor
lines.push(BufferLine.new(new_cols, null_cell))
else: # _rows >= new_rows
for _y in range(_rows, new_rows, -1):
if lines.length > new_rows + ybase:
if lines.length > ybase + y + 1:
# The line is blank line below the cursor, remove it
lines.pop()
else:
# The line is the cursor, scroll down
ybase += 1
ydisp += 1
# Reduce max length if needed after adjustments, this is done after as it
# would otherwise cut data from the bottom of the buffer.
if new_max_length < lines.max_length:
# Trim from the top of th ebuffer and adjust ybase and ydisp.
var amount_to_trim = lines.length - new_max_length
if amount_to_trim > 0:
lines.trim_start(amount_to_trim)
ybase = max(ybase - amount_to_trim, 0)
ydisp = max(ydisp - amount_to_trim, 0)
saved_y = max(saved_y - amount_to_trim, 0)
lines.max_length = new_max_length
# Make sure that the cursor stays on screen
x = min(x, new_cols - 1)
y = min(y, new_rows - 1)
if add_to_y:
y += add_to_y
saved_x = min(saved_x, new_cols -1)
scroll_top = 0
scroll_bottom = new_rows - 1
if _is_reflow_enabled():
_reflow(new_cols, new_rows)
# Trim the end of the line off if cols shrunk
if _cols > new_cols:
for i in range(lines.length):
lines.get_line(i).resize(new_cols, null_cell)
_cols = new_cols
_rows = new_rows
func _is_reflow_enabled() -> bool:
return _has_scrollback and not _options_service.options.windows_mode
func _reflow(new_cols: int, new_rows: int) -> void:
if _cols == new_cols:
return
# Iterate through rows, ignore the last one as it cannot be wrapped
if new_cols > _cols:
_reflow_larger(new_cols, new_rows)
else:
_reflow_smaller(new_cols, new_rows)
func _reflow_larger(new_cols: int, new_rows: int) -> void:
var to_remove: PoolIntArray = BufferReflow.reflow_larger_get_lines_to_remove(lines,
_cols, new_cols, ybase + y, get_null_cell(AttributeData.new()))
if not to_remove.empty():
var new_layout_result = BufferReflow.reflow_larger_create_new_layout(lines, to_remove)
BufferReflow.reflow_larger_apply_new_layout(lines, new_layout_result.layout)
_reflow_larger_adjust_viewport(new_cols, new_rows, new_layout_result.count_removed)
func _reflow_larger_adjust_viewport(new_cols: int, new_rows: int, count_removed: int) -> void:
var null_cell = get_null_cell(AttributeData.new())
# Adjust viewport based on number of items removed
var viewport_adjustments = count_removed
while viewport_adjustments > 0:
viewport_adjustments -= 1
if ybase == 0:
if y > 0:
y -= 1
if lines.length < new_rows:
# Add an extra row at the bottom of the viewport
lines.push(BufferLine.new(new_cols, null_cell))
else:
if ydisp == ybase:
ydisp -= 1
ybase -= 1
saved_y = max(saved_y - count_removed, 0)
func _reflow_smaller(new_cols: int, new_rows: int) -> void:
var null_cell = get_null_cell(AttributeData.new())
# Gather all BufferLines that need to be inserted into the Buffer here so that they can be
# batched up and only commited once
var to_insert = []
var count_to_insert = 0
# Go backwards as many lines may be trimmed and this will avoid considering them
var i = lines.length - 1
while i >= 0:
# Check wether this line is a problem
var next_line = lines.get_line(i)
if not next_line or not next_line.is_wrapped and next_line.get_trimmed_length() <= new_cols:
i -= 1
continue
# Gather wrapped lines and adjust y to be the starting line
var wrapped_lines = [next_line]
while next_line.is_wrapped and i > 0:
i -= 1
next_line = lines.get_line(i)
wrapped_lines.push_front(next_line)
# If these lines contain the cursor don't touch them, the program will handle fixing up
# wrapped lines with the cursor
var absolute_y = ybase + y
if absolute_y >= i and absolute_y < i + wrapped_lines.size():
i -= 1
continue
var last_line_length = wrapped_lines[wrapped_lines.size() - 1].get_trimmed_length()
var dest_line_lengths = BufferReflow.reflow_smaller_get_new_line_lengths(wrapped_lines, _cols, new_cols)
var lines_to_add = dest_line_lengths.size() - wrapped_lines.size()
var trimmed_lines: int
if ybase == 0 and y != lines.length - 1:
# If the top section of the buffer is not yet filled
trimmed_lines = max(0, y - lines.max_length + lines_to_add)
else:
trimmed_lines = max(0, lines.length - lines.max_length + lines_to_add)
# Add the new lines
var new_lines = []
for j in range(lines_to_add):
var new_line = get_blank_line(AttributeData.new(), true)
new_lines.append(new_line)
if not new_lines.empty():
to_insert.append({"start": i + wrapped_lines.size() + count_to_insert,
"new_lines": new_lines})
count_to_insert += new_lines.size()
wrapped_lines += new_lines
# Copy buffer data to new locations, this needs to happen backwards to do in-place
var dest_line_index = dest_line_lengths.size() - 1 # floor(cells_needed / new_cols)
var dest_col = dest_line_lengths[dest_line_index] # cells_needed % new_cols
if dest_col == 0:
dest_line_index -= 1
dest_col = dest_line_lengths[dest_line_index]
var src_line_index = wrapped_lines.size() - lines_to_add - 1
var src_col = last_line_length
while src_line_index >= 0:
var cells_to_copy = min(src_col, dest_col)
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[src_line_index],
src_col - cells_to_copy, dest_col - cells_to_copy, cells_to_copy, true)
dest_col -= cells_to_copy
if dest_col == 0:
dest_line_index -= 1
dest_col = dest_line_lengths[dest_line_index]
src_col -= cells_to_copy
if src_col == 0:
src_line_index -= 1
var wrapped_lines_index = max(src_line_index, 0)
src_col = BufferReflow.get_wrapped_line_trimmed_length(wrapped_lines, wrapped_lines_index, _cols)
# Null out the end of the line ends if a wide character wrapped to the following line
for j in range(wrapped_lines.size()):
if dest_line_lengths[j] < new_cols:
wrapped_lines[j].set_cell(dest_line_lengths[j], null_cell)
# Adjust viewport as needed
var viewport_adjustments = lines_to_add - trimmed_lines
while viewport_adjustments > 0:
if ybase == 0:
if y < new_rows - 1:
y += 1
lines.pop()
else:
ybase += 1
ydisp += 1
else:
# Ensure ybase does not exceed its maximum value
if ybase < min(lines.max_length, (lines.length + count_to_insert) - new_rows):
if ybase == ydisp:
ydisp += 1
ybase += 1
viewport_adjustments -= 1
saved_y = min(saved_y + lines_to_add, ybase + new_rows - 1)
i -= 1
# Rearrange lines in the buffer if there are any insertions, this is done at the end rather
# than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
# costly calls to CircularList.splice.
if not to_insert.empty():
# Record buffer insert events and then play them backwards so that the indexes are
# correct
var insert_events = []
# Record original lines so they don't get overridden when we rearrange the list
var original_lines = []
for i in range(lines.length):
original_lines.append(lines.get_line(i))
var original_lines_length = lines.length
var original_line_index = original_lines_length - 1
var next_to_insert_index = 0
var next_to_insert = to_insert[next_to_insert_index]
lines.length = min(lines.max_length, lines.length + count_to_insert)
var count_inserted_so_far = 0
var j = min(lines.max_length - 1, original_lines_length + count_to_insert - 1)
while j >= 0:
if next_to_insert and next_to_insert.start > original_line_index + count_inserted_so_far:
# Insert extra lines here, adjusting i as needed
for next_i in range(next_to_insert.new_lines.size() - 1, -1, -1):
lines.set_line(j, next_to_insert.new_lines[next_i])
j -= 1
j += 1
# Create insert events for later
insert_events.append({"index": original_line_index + 1,
"amount": next_to_insert.new_lines.size()})
count_inserted_so_far += next_to_insert.new_lines.size()
next_to_insert_index += 1
next_to_insert = to_insert[next_to_insert_index] if to_insert.size() > next_to_insert_index else null
else:
lines.set_line(j, original_lines[original_line_index])
original_line_index -= 1
j -= 1
# Update markers
var insert_count_emitted = 0
for i in range(insert_events.size() - 1, -1, -1):
insert_events[i].index += insert_count_emitted
lines.emit_signal("inserted", insert_events[i])
insert_count_emitted += insert_events[i].amount
var amount_to_trim = max(0, original_lines_length + count_to_insert - lines.max_length)
if amount_to_trim > 0:
lines.emit_signal("trimmed", amount_to_trim)
func get_null_cell(attr = null):
if attr:
_null_cell.fg = attr.fg
@ -111,10 +391,10 @@ func get_wrapped_range_for_line(y: int) -> Dictionary:
return {"first": first, "last": last}
# Setup the tab stops.
# @param i The index to start setting up tab stops from.
func setup_tab_stops(i = null) -> void:
if i == null:
return
if i != null:
if not tabs.get(i):
i = prev_stop(i)
else:
@ -123,9 +403,11 @@ func setup_tab_stops(i = null) -> void:
while i < _cols:
tabs[i] = true
i += _options_service.options.tab_stop_width
i += max(_options_service.options.tab_stop_width, 1)
# Move the cursor to the previous tab stop from the given position (default is current).
# @param x The position to move the cursor to the previous tab stop.
func prev_stop(x: int) -> int:
if x == null:
x = self.x
@ -135,7 +417,14 @@ func prev_stop(x: int) -> int:
return _cols - 1 if x > _cols else 0 if x < 0 else x
# Move the cursor one tab stop forward from the given position (default is current).
# @param x The position to move the cursor one tab stop forward.
func next_stop(x = null) -> int:
if x == null:
x = self.x
x += 1
while not tabs.get(x) and x < _cols:
x += 1
return _cols - 1 if x >= _cols else 0 if x < 0 else x

View file

@ -41,7 +41,10 @@ func get_cell(index: int):
func get_width(index: int) -> int:
if (index * CELL_SIZE + Cell.CONTENT) < _data.size():
return _data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT
else:
return 0
func has_content(index: int) -> int:
@ -221,13 +224,43 @@ func fill(fill_cell_data) -> void:
set_cell(i, fill_cell_data)
# alter to a full copy of line
func copy_from(line) -> void:
_data = line._data.duplicate()
length = line.length
_combined = {}
for k in line._combined.keys():
_combined[k] = line._combined[k]
is_wrapped = line.is_wrapped
func get_trimmed_length() -> int:
for i in range(length - 1, 0, -1):
for i in range(length - 1, -1, -1):
if _data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK:
return i + (_data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT)
return 0
func copy_cells_from(src, src_col: int, dest_col: int, length: int, apply_in_reverse: bool) -> void:
var src_data = src._data
if apply_in_reverse:
for cell in range(length - 1, -1, -1):
for i in range(CELL_SIZE):
_data[(dest_col + cell) * CELL_SIZE + i] = src_data[(src_col + cell) * CELL_SIZE + i]
else:
for cell in range(length):
for i in range(CELL_SIZE):
_data[(dest_col + cell) * CELL_SIZE + i] = src_data[(src_col + cell) * CELL_SIZE + i]
# Move any combined data over as needed, FIXME: repeat for extended attrs
var src_combined_keys = src._combined.keys()
for i in range(src_combined_keys.size()):
var key = int(src_combined_keys[i])
if key >= src_col:
_combined[key + src_col + dest_col] = src._combined[key]
func translate_to_string(trim_right: bool = false, start_col: int = 0, end_col: int = -1) -> String:
if end_col == -1:
end_col = length
@ -245,3 +278,14 @@ func translate_to_string(trim_right: bool = false, start_col: int = 0, end_col:
result += Constants.WHITESPACE_CELL_CHAR
start_col += max(content >> Content.WIDTH_SHIFT, 1) # always advance by 1
return result
func duplicate():
# Workaround as per: https://github.com/godotengine/godot/issues/19345#issuecomment-471218401
var duplicant = load("res://addons/godot_xterm/buffer/buffer_line.gd").new(length)
duplicant._data = _data.duplicate(true)
duplicant._combined = _combined.duplicate(true)
duplicant._extended_attrs = _extended_attrs.duplicate(true)
duplicant.length = length
duplicant.is_wrapped = is_wrapped
return duplicant

View file

@ -0,0 +1,198 @@
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends Object
# Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed
# when a wrapped line unwraps.
# @param lines The buffer lines.
# @param newCols The columns after resize.
static func reflow_larger_get_lines_to_remove(lines, old_cols: int, new_cols: int,
buffer_absolute_y: int, null_cell) -> PoolIntArray:
# Gather all BufferLines that need to be removed from the Buffer here so that they can be
# batched up and only committed once
var to_remove = PoolIntArray([])
var y = 0
while y < lines.length - 1:
# Check if this row is wrapped
var i = y
i += 1
var next_line = lines.get_line(i)
if not next_line.is_wrapped:
y += 1
continue
# Check how many lines it's wrapped for
var wrapped_lines = [lines.get_line(y)]
while i < lines.length and next_line.is_wrapped:
wrapped_lines.append(next_line)
i += 1
next_line = lines.get_line(i)
# If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
# lines with the cursor
if buffer_absolute_y >= y and buffer_absolute_y < i:
y += wrapped_lines.size() - 1
y += 1
continue
# Copy buffer data to new locations
var dest_line_index = 0
var dest_col = get_wrapped_line_trimmed_length(wrapped_lines, dest_line_index, old_cols)
var src_line_index = 1
var src_col = 0
while src_line_index < wrapped_lines.size():
var src_trimmed_line_length = get_wrapped_line_trimmed_length(wrapped_lines, src_line_index, old_cols)
var src_remaining_cells = src_trimmed_line_length - src_col
var dest_remaining_cells = new_cols - dest_col
var cells_to_copy = min(src_remaining_cells, dest_remaining_cells)
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[src_line_index], src_col, dest_col, cells_to_copy, false)
dest_col += cells_to_copy
if dest_col == new_cols:
dest_line_index += 1
dest_col = 0
src_col += cells_to_copy
if src_col == src_trimmed_line_length:
src_line_index += 1
src_col = 0
# Make sure the last cell isn't wide, if it is copy it to the current dest
if dest_col == 0 and dest_line_index != 0:
if wrapped_lines[dest_line_index - 1].get_width(new_cols - 1) == 2:
wrapped_lines[dest_line_index].copy_cells_from(wrapped_lines[dest_line_index - 1], new_cols - 1, dest_col, 1, false)
dest_col += 1
# Null out the end of the last row
wrapped_lines[dest_line_index - 1].set_cell(new_cols - 1, null_cell)
# Clear out remaining cells or fragments could remain;
var replaced = wrapped_lines[dest_line_index].translate_to_string()
wrapped_lines[dest_line_index].replace_cells(dest_col, new_cols, null_cell)
# Work backwards and remove any rows at the end that only contain null cells
var count_to_remove = 0
for i in range(wrapped_lines.size() - 1, 0, -1):
if i > dest_line_index or wrapped_lines[i].get_trimmed_length() == 0:
count_to_remove += 1
else:
break
if count_to_remove > 0:
to_remove.append(y + wrapped_lines.size() - count_to_remove) # index
to_remove.append(count_to_remove)
y += wrapped_lines.size() - 1
y += 1
return to_remove
# Creates and return the new layout for lines given an array of indexes to be removed.
# @param lines The buffer lines.
# @param to_remove The indexes to remove.
static func reflow_larger_create_new_layout(lines, to_remove: PoolIntArray):
var layout = PoolIntArray([])
# First iterate through the list and get the actual indexes to use for rows
var next_to_remove_index = 0
var next_to_remove_start = to_remove[next_to_remove_index]
var count_removed_so_far = 0
var i = 0
while i < lines.length:
if next_to_remove_start == i:
next_to_remove_index += 1
var count_to_remove = to_remove[next_to_remove_index]
# Tell markers that there was a deletion
lines.emit_signal("deleted", i - count_removed_so_far, count_to_remove)
i += count_to_remove - 1
count_removed_so_far += count_to_remove
next_to_remove_index += 1
next_to_remove_start = to_remove[next_to_remove_index] if next_to_remove_index < to_remove.size() else null
else:
layout.append(i)
i += 1
return { "layout": layout, "count_removed": count_removed_so_far }
# Applies a new layout to the buffer. This essentially does the same as many splice calls but it's
# done all at once in a single iteration through the list since splice is very expensive.
# @param lines The buffer lines.
# @param new_layout The new layout to apply.
static func reflow_larger_apply_new_layout(lines, new_layout: PoolIntArray) -> void:
# Record original lines so they don't get overridden when we rearrange the list
var new_layout_lines = []
for i in range(new_layout.size()):
new_layout_lines.append(lines.get_line(new_layout[i]))
# Rearrange the list
for i in range(new_layout_lines.size()):
lines.set_line(i, new_layout_lines[i])
lines.length = new_layout.size()
# Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
# compute the wrapping points since wide characters may need to be wrapped onto the following line.
# This function will return an array of numbers of where each line wraps to, the resulting array
# will only contain the values `newCols` (when the line does not end with a wide character) and
# `new_cols - 1` (when the line does end with a wide character), except for the last value which
# will contain the remaining items to fill the line.
#
# Calling this with a `new_cols` value of `1` will lock up.
#
# @param wrapped_lines The wrapped lines to evaluate.
# @param old_cols The columns before resize.
# @param new_cols The columns after resize.
static func reflow_smaller_get_new_line_lengths(wrapped_lines: Array, old_cols: int, new_cols: int) -> PoolIntArray:
var new_line_lengths = PoolIntArray([])
var cells_needed: int
for i in range(wrapped_lines.size()):
cells_needed += get_wrapped_line_trimmed_length(wrapped_lines, i, old_cols)
# Use src_col and scr_line to find the new wrapping point, use that to get the cells_available and
# lines_needed
var src_col = 0
var src_line = 0
var cells_available = 0
while cells_available < cells_needed:
if cells_needed - cells_available < new_cols:
# Add the final line and exit the loop
new_line_lengths.append(cells_needed - cells_available)
break
src_col += new_cols
var old_trimmed_length = get_wrapped_line_trimmed_length(wrapped_lines, src_line, old_cols)
if src_col > old_trimmed_length:
src_col -= old_trimmed_length
src_line += 1
var ends_with_wide = wrapped_lines[src_line].get_width(src_col - 1) == 2
if ends_with_wide:
src_col -= 1
var line_length = new_cols - 1 if ends_with_wide else new_cols
new_line_lengths.append(line_length)
cells_available += line_length
return new_line_lengths
static func get_wrapped_line_trimmed_length(lines: Array, i: int, cols: int) -> int:
# If this is the last row in the wrapped line, get the actual trimmed length
if i == lines.size() - 1:
return lines[i].get_trimmed_length()
# Detect whether the following line starts with a wide character and the end of the current line
# is null, if so then we can be pretty sure the null character should be excluded from the line
# length
var ends_in_null = not (lines[i].has_content(cols - 1)) and lines[i].get_width(cols - 1) == 1
var following_line_starts_with_wide = lines[i + 1].get_width(0) == 2
if ends_in_null and following_line_starts_with_wide:
return cols - 1
return cols

View file

@ -21,8 +21,7 @@ func _init(options_service, buffer_service):
alt = Buffer.new(false, options_service, buffer_service)
active = normal
# TODO
#setup_tab_stops()
setup_tab_stops()
# Sets the normal Bufer of the BufferSet as its currently active Buffer.
@ -53,3 +52,18 @@ func activate_alt_buffer(fill_attr = null) -> void:
alt.y = normal.y
active = alt
emit_signal("buffer_activated", alt, normal)
# Resizes both normal and alt buffers, adjusting their data accordingly.
# @param new_cols The new number of columns.
# @param new_rows The new number of rows.
func resize(new_cols: int, new_rows: int) -> void:
normal.resize(new_cols, new_rows)
alt.resize(new_cols, new_rows)
# Setup the tab stops.
# @param i The index to start setting up tab stops from.
func setup_tab_stops(i = null) -> void:
normal.setup_tab_stops(i)
alt.setup_tab_stops(i)

View file

@ -91,3 +91,16 @@ enum UnderlineStyle {
DOTTED
DASHED
}
enum CursorStyle {
BLOCK
UNDERLINE
BAR
}
enum BellStyle {
NONE
VISUAL
SOUND
BOTH
}

View file

@ -5,7 +5,8 @@ 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 deleted(index, amount)
signal inserted
signal trimmed
@ -13,6 +14,7 @@ var _array
var _start_index: int
var length: int = 0 setget _set_length,_get_length
var max_length: int setget _set_max_length,_get_max_length
var is_full: bool setget ,_get_is_full
func _set_length(new_length: int):
@ -45,7 +47,12 @@ func _get_max_length():
return max_length
func _init(max_length):
# Ringbuffer is at max length.
func _get_is_full() -> bool:
return length == max_length
func _init(max_length = 0):
self.max_length = max_length
_array = []
_array.resize(max_length)
@ -56,10 +63,23 @@ func get_el(index: int):
return _array[_get_cyclic_index(index)]
# Alias for `get_al`.
func get_line(index: int):
return get_el(index)
func set_el(index: int, value) -> void:
_array[_get_cyclic_index(index)] = value
# Alias for `set_el`.
func set_line(index: int, value) -> void:
set_el(index, value)
# Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0
# if the maximum length is reached.
# @param value The value to push onto the list.
func push(value) -> void:
_array[_get_cyclic_index(length)] = value
if length == max_length:
@ -70,6 +90,96 @@ func push(value) -> void:
length += 1
func _get_cyclic_index(index: int) -> int:
return _start_index + index % max_length
# Advance ringbuffer index and return current element for recycling.
# Note: The buffer must be full for this method to work.
# @throws When the buffer is not full.
func recycle():
if length != max_length:
push_error("Can only recycle when the buffer is full")
_start_index = (_start_index + 1) % max_length
emit_signal("trimmed", 1)
return _array[_get_cyclic_index(length - 1)]
# Removes and returns the last value on the list.
# @return The popped value.
func pop():
var last = _array[_get_cyclic_index(length - 1)]
length -= 1
return last
# Deletes and/or inserts items at a particular index (in that order). Unlike
# Array.prototype.splice, this operation does not return the deleted items as a new array in
# order to save creating a new array. Note that this operation may shift all values in the list
# in the worst case.
# @param start The index to delete and/or insert.
# @param deleteCount The number of elements to delete.
# @param items The items to insert.
func splice(start: int, delete_count: int, items: Array = []) -> void:
# Delete items
if delete_count:
for i in range(start, length - delete_count):
_array[_get_cyclic_index(i)] = _array[_get_cyclic_index(i + delete_count)]
length -= delete_count
# Add items
var i = length - 1
while i >= start:
_array[_get_cyclic_index(i + items.size())] = _array[_get_cyclic_index(i)]
i -= 1
for i in range(items.size()):
_array[_get_cyclic_index(start + i)] = items[i]
# Adjust length as needed
if length + items.size() > max_length:
var count_to_trim = (length + items.size()) - max_length
_start_index += count_to_trim
length = max_length
emit_signal("trimmed", count_to_trim)
else:
length += items.size()
# Trims a number of items from the start of the list.
# @param count The number of items to remove.
func trim_start(count: int) -> void:
if count > length:
count = length
_start_index += count
length -= count
emit_signal("trimmed", count)
func shift_elements(start: int, count: int, offset: int) -> void:
if count <= 0:
return
if start < 0 or start >= length:
self.push_error("start argument out of range")
if start + offset < 0:
self.push_error("cannot shift elements in list beyond index 0")
if offset > 0:
for i in range(count - 1, -1, -1):
set_el(start + i + offset, get_el(start + i))
var expand_list_by = (start + count + offset) - length
if expand_list_by > 0:
length += expand_list_by
while length > max_length:
length -= 1
_start_index += 1
emit_signal("trimmed", 1)
else:
for i in range(0, count):
set_el(start + i + offset, get_el(start + i))
func _get_cyclic_index(index: int) -> int:
return (_start_index + index) % max_length
# Wrapper for `push_error` so we can test for calls to this built-in function.
func push_error(message):
push_error(message)

View file

@ -35,12 +35,12 @@ static func _generate_default_ansi_colors() -> PoolColorArray:
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))
colors.append(Color("%02x%02x%02x" % [r, g, b]))
# Generate greys (232-255)
for i in range(0, 24):
var c = 8 + i * 10
colors.append(Color(c, c, c))
colors.append(Color("%02x%02x%02x" % [c, c, c]))
return colors

View file

@ -2,23 +2,255 @@
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends Reference
# The character sets supported by the terminal. These enable several languages
# to be represented within the terminal with only 8-bit encoding. See ISO 2022
# for a discussion on character sets. Only VT100 character sets are supported.
const CHARSETS = {
# DEC Special Character and Line Drawing Set.
# Reference: http:#vt100.net/docs/vt102-ug/table5-13.html
# A lot of curses apps use this if they see TERM=xterm.
# testing: echo -e "\e(0a\e(B"
# The xterm output sometimes seems to conflict with the
# reference above. xterm seems in line with the reference
# when running vttest however.
# The table below now uses xterm"s output from vttest.
"0": {
"`": "\u25c6", # "◆"
"a": "\u2592", # "▒"
"b": "\u2409", # "␉" (HT)
"c": "\u240c", # "␌" (FF)
"d": "\u240d", # "␍" (CR)
"e": "\u240a", # "␊" (LF)
"f": "\u00b0", # "°"
"g": "\u00b1", # "±"
"h": "\u2424", # "␤" (NL)
"i": "\u240b", # "␋" (VT)
"j": "\u2518", # "┘"
"k": "\u2510", # "┐"
"l": "\u250c", # "┌"
"m": "\u2514", # "└"
"n": "\u253c", # "┼"
"o": "\u23ba", # "⎺"
"p": "\u23bb", # "⎻"
"q": "\u2500", # "─"
"r": "\u23bc", # "⎼"
"s": "\u23bd", # "⎽"
"t": "\u251c", # "├"
"u": "\u2524", # "┤"
"v": "\u2534", # "┴"
"w": "\u252c", # "┬"
"x": "\u2502", # "│"
"y": "\u2264", # "≤"
"z": "\u2265", # "≥"
"{": "\u03c0", # "π"
"|": "\u2260", # "≠"
"}": "\u00a3", # "£"
"~": "\u00b7" # "·"
},
# British character set
# ESC (A
# Reference: http://vt100.net/docs/vt220-rm/table2-5.html
'A': {
'#': '£'
# Reference: http:#vt100.net/docs/vt220-rm/table2-5.html
"A": {
"#": "£"
},
# United States character set
# ESC (B
'B': null,
"B": null,
# Dutch character set
# ESC (4
# Reference: http:#vt100.net/docs/vt220-rm/table2-6.html
"4": {
"#": "£",
"@": "¾",
"[": "ij",
"\\": "½",
"]": "|",
"{": "¨",
"|": "f",
"}": "¼",
"~": "´"
},
# Finnish character set
# ESC (C or ESC (5
# Reference: http:#vt100.net/docs/vt220-rm/table2-7.html
"C": {
"[": "Ä",
"\\": "Ö",
"]": "Å",
"^": "Ü",
"`": "é",
"{": "ä",
"|": "ö",
"}": "å",
"~": "ü"
},
"5": {
"[": "Ä",
"\\": "Ö",
"]": "Å",
"^": "Ü",
"`": "é",
"{": "ä",
"|": "ö",
"}": "å",
"~": "ü"
},
# French character set
# ESC (R
# Reference: http:#vt100.net/docs/vt220-rm/table2-8.html
"R": {
"#": "£",
"@": "à",
"[": "°",
"\\": "ç",
"]": "§",
"{": "é",
"|": "ù",
"}": "è",
"~": "¨"
},
# French Canadian character set
# ESC (Q
# Reference: http:#vt100.net/docs/vt220-rm/table2-9.html
"Q": {
"@": "à",
"[": "â",
"\\": "ç",
"]": "ê",
"^": "î",
"`": "ô",
"{": "é",
"|": "ù",
"}": "è",
"~": "û"
},
# German character set
# ESC (K
# Reference: http:#vt100.net/docs/vt220-rm/table2-10.html
"K": {
"@": "§",
"[": "Ä",
"\\": "Ö",
"]": "Ü",
"{": "ä",
"|": "ö",
"}": "ü",
"~": "ß"
},
# Italian character set
# ESC (Y
# Reference: http:#vt100.net/docs/vt220-rm/table2-11.html
"Y": {
"#": "£",
"@": "§",
"[": "°",
"\\": "ç",
"]": "é",
"`": "ù",
"{": "à",
"|": "ò",
"}": "è",
"~": "ì"
},
# Norwegian/Danish character set
# ESC (E or ESC (6
# Reference: http:#vt100.net/docs/vt220-rm/table2-12.html
"E": {
"@": "Ä",
"[": "Æ",
"\\": "Ø",
"]": "Å",
"^": "Ü",
"`": "ä",
"{": "æ",
"|": "ø",
"}": "å",
"~": "ü"
},
"6": {
"@": "Ä",
"[": "Æ",
"\\": "Ø",
"]": "Å",
"^": "Ü",
"`": "ä",
"{": "æ",
"|": "ø",
"}": "å",
"~": "ü"
},
# Spanish character set
# ESC (Z
# Reference: http:#vt100.net/docs/vt220-rm/table2-13.html
"Z": {
"#": "£",
"@": "§",
"[": "¡",
"\\": "Ñ",
"]": "¿",
"{": "°",
"|": "ñ",
"}": "ç"
},
# Swedish character set
# ESC (H or ESC (7
# Reference: http:#vt100.net/docs/vt220-rm/table2-14.html
"H": {
"@": "É",
"[": "Ä",
"\\": "Ö",
"]": "Å",
"^": "Ü",
"`": "é",
"{": "ä",
"|": "ö",
"}": "å",
"~": "ü"
},
"7": {
"@": "É",
"[": "Ä",
"\\": "Ö",
"]": "Å",
"^": "Ü",
"`": "é",
"{": "ä",
"|": "ö",
"}": "å",
"~": "ü"
},
# Swiss character set
# ESC (=
# Reference: http:#vt100.net/docs/vt220-rm/table2-15.html
#/
"=": {
"#": "ù",
"@": "à",
"[": "é",
"\\": "ç",
"]": "ê",
"^": "î",
"_": "è",
"`": "ô",
"{": "ä",
"|": "ö",
"}": "ü",
"~": "û"
},
}
# The default character set, US.
const DEFAULT_CHARSET = CHARSETS['B']
const DEFAULT_CHARSET = CHARSETS["B"]

View file

@ -3,5 +3,4 @@
[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 )

View file

@ -3,6 +3,11 @@
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends Reference
# The terminal's InputHandler, this handles all input from the Parser.
#
# Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand
# each function's header comment.
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
const BufferConstants = preload("res://addons/godot_xterm/buffer/constants.gd")
@ -18,6 +23,9 @@ const C1 = Constants.C1
const FgFlags = BufferConstants.FgFlags
const BgFlags = BufferConstants.BgFlags
const UnderlineStyle = BufferConstants.UnderlineStyle
const CursorStyle = BufferConstants.CursorStyle
const CHARSETS = Charsets.CHARSETS
const Content = BufferConstants.Content
const GLEVEL = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2}
const MAX_PARSEBUFFER_LENGTH = 131072
@ -69,72 +77,100 @@ func _init(buffer_service, core_service, charset_service, options_service,
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 handler
_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")
# 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')
# execute handler
_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, "tab");
_parser.set_execute_handler(C0.SO, self, "shift_out")
_parser.set_execute_handler(C0.SI, self, "shift_in")
# FIXME: What to do with missing? Old code just added those to print
_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")
# ESC handlers
_parser.set_esc_handler({"final": "7"}, self, "save_cursor")
_parser.set_esc_handler({"final": "8"}, self, "restore_cursor")
_parser.set_esc_handler({"final": "D"}, self, "index")
_parser.set_esc_handler({"final": "E"}, self, "next_line")
_parser.set_esc_handler({"final": "H"}, self, "tab_set")
_parser.set_esc_handler({"final": "M"}, self, "reverse_index")
_parser.set_esc_handler({"final": "="}, self, "keypad_application_mode")
_parser.set_esc_handler({"final": ">"}, self, "keypad_numeric_mode")
_parser.set_esc_handler({"final": "c"}, self, "full_reset")
_parser.set_esc_handler({"final": "n"}, self, "set_glevel", 2)
_parser.set_esc_handler({"final": "o"}, self, "set_glevel", 3)
_parser.set_esc_handler({"final": "|"}, self, "set_glevel", 3)
_parser.set_esc_handler({"final": "}"}, self, "set_glevel", 2)
_parser.set_esc_handler({"final": "~"}, self, "set_glevel", 1)
_parser.set_esc_handler({"intermediates": "%", "final": "@"}, self, "select_default_charset")
_parser.set_esc_handler({"intermediates": "%", "final": "G"}, self, "select_default_charset")
for flag in CHARSETS.keys():
_parser.set_esc_handler({"intermediates": "(", "final": flag}, self, "select_charset", "(" + flag)
_parser.set_esc_handler({"intermediates": ")", "final": flag}, self, "select_charset", ")" + flag)
_parser.set_esc_handler({"intermediates": "*", "final": flag}, self, "select_charset", "*" + flag)
_parser.set_esc_handler({"intermediates": "+", "final": flag}, self, "select_charset", "+" + flag)
_parser.set_esc_handler({"intermediates": "-", "final": flag}, self, "select_charset", "-" + flag)
_parser.set_esc_handler({"intermediates": ".", "final": flag}, self, "select_charset", "." + flag)
_parser.set_esc_handler({"intermediates": "/", "final": flag}, self, "select_charset", "/" + flag) # TODO: supported?
_parser.set_esc_handler({"intermediates": "#", "final": "8"}, self, "screen_alignment_pattern")
#func parse(data) -> void:
@ -224,7 +260,7 @@ func print(data: Array, start: int, end: int) -> void:
# 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)]
var ch = charset.get(char(code))
if ch:
code = ch.ord_at(0)
@ -334,7 +370,7 @@ func line_feed():
buffer.y += 1
if buffer.y == buffer.scroll_bottom + 1:
buffer.y -= 1
emit_signal("scroll_requested")
emit_signal("scroll_requested", _erase_attr_data())
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.
@ -400,12 +436,22 @@ func tab():
# TODO A11y
# SO
# Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the
# G1 character set.
#
# @vt: #P[Only limited ISO-2022 charset support.] C0 SO "Shift Out" "\x0E" "Switch to an alternative character set."
func shift_out():
_charset_service.set_glevel(1)
# SI
# Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0
# character set (the default).
#
# @vt: #Y C0 SI "Shift In" "\x0F" "Return to regular character set after Shift Out."
func shift_in():
_charset_service.get_glevel(0)
_charset_service.set_glevel(0)
func _restrict_cursor(max_col: int = _buffer_service.cols - 1) -> void:
@ -439,20 +485,142 @@ func _move_cursor(x: int, y: int) -> void:
_set_cursor(self._buffer.y + x, self._buffer.y + y)
func index():
print("TODO: index")
# ESC D
# C1.IND
# DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html)
# Moves the cursor down one line in the same column.
#
# @vt: #Y C1 IND "Index" "\x84" "Move the cursor one line down scrolling if needed."
# @vt: #Y ESC IND "Index" "ESC D" "Move the cursor one line down scrolling if needed."
func index() -> void:
_restrict_cursor()
buffer.y += 1
if buffer.y == buffer.scroll_bottom + 1:
buffer.y -= 1
emit_signal("scroll_requested", _erase_attr_data())
elif buffer.y >= _buffer_service.rows:
buffer.y = _buffer_service.rows - 1
_restrict_cursor()
func next_line():
print("TODO: next_line")
# ESC E
# C1.NEL
# DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL)
# Moves cursor to first position on next line.
#
# @vt: #Y C1 NEL "Next Line" "\x85" "Move the cursor to the beginning of the next row."
# @vt: #Y ESC NEL "Next Line" "ESC E" "Move the cursor to the beginning of the next row."
func next_line() -> void:
buffer.x = 0
index()
# ESC =
# DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html)
# Enables the numeric keypad to send application sequences to the host.
func keypad_application_mode() -> void:
_core_service.dec_private_modes.application_keypad = true
emit_signal("scrollbar_sync_requested")
# ESC >
# DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html)
# Enables the keypad to send numeric characters to the host.
func keypad_numeric_mode() -> void:
_core_service.dec_private_modes.application_keypad = false
emit_signal("scrollbar_sync_requested")
# ESC % @
# ESC % G
# Select default character set. UTF-8 is not supported (string are unicode anyways)
# therefore ESC % G does the same.
func select_default_charset() -> void:
_charset_service.set_glevel(0)
_charset_service.set_gcharset(0, Charsets.DEFAULT_CHARSET) # US (default)
# ESC ( C
# Designate G0 Character Set, VT100, ISO 2022.
# ESC ) C
# Designate G1 Character Set (ISO 2022, VT100).
# ESC * C
# Designate G2 Character Set (ISO 2022, VT220).
# ESC + C
# Designate G3 Character Set (ISO 2022, VT220).
# ESC - C
# Designate G1 Character Set (VT300).
# ESC . C
# Designate G2 Character Set (VT300).
# ESC / C
# Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported?
func select_charset(collect_and_flag: String) -> void:
if collect_and_flag.length() != 2:
select_default_charset()
return
if collect_and_flag.substr(0, 1) == "/":
return # TODO: Is this supported?
var g = GLEVEL[collect_and_flag.substr(0, 1)]
var charset = CHARSETS[collect_and_flag.substr(1, 1)]
_charset_service.set_gcharset(g, charset)
# ESC H
# C1.HTS
# DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html)
# Sets a horizontal tab stop at the column position indicated by
# the value of the active column when the terminal receives an HTS.
#
# @vt: #Y C1 HTS "Horizontal Tabulation Set" "\x88" "Places a tab stop at the current cursor position."
# @vt: #Y ESC HTS "Horizontal Tabulation Set" "ESC H" "Places a tab stop at the current cursor position."
func tab_set():
print("TODO: tab_set")
buffer.tabs[buffer.x] = true
func insert_chars(params):
print("TODO: insert_chars")
func scroll_left(params):
print("TODO: scroll_left")
# CSI Ps @
# Insert Ps (Blank) Character(s) (default = 1) (ICH).
#
# @vt: #Y CSI ICH "Insert Characters" "CSI Ps @" "Insert `Ps` (blank) characters (default = 1)."
# The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the blank characters.
# Text between the cursor and right margin moves to the right. Characters moved past the right margin are lost.
#
#
# FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)
func insert_chars(params) -> void:
_restrict_cursor()
var line = buffer.lines.get_el(buffer.ybase + buffer.y)
if line:
line.insert_cells(buffer.x, params.get_param(0, 1),
buffer.get_null_cell(_erase_attr_data()), _erase_attr_data())
# CSI Ps SP @ Scroll left Ps columns (default = 1) (SL) ECMA-48
#
# Notation: (Pn)
# Representation: CSI Pn 02/00 04/00
# Parameter default value: Pn = 1
# SL causes the data in the presentation component to be moved by n character positions
# if the line orientation is horizontal, or by n line positions if the line orientation
# is vertical, such that the data appear to move to the left; where n equals the value of Pn.
# The active presentation position is not affected by this control function.
#
# Supported:
# - always left shift (no line orientation setting respected)
#
# @vt: #Y CSI SL "Scroll Left" "CSI Ps SP @" "Scroll viewport `Ps` times to the left."
# SL moves the content of all lines within the scroll margins `Ps` times to the left.
# SL has no effect outside of the scroll margins.
func scroll_left(params) -> void:
if buffer.y > buffer.scroll_bottom or buffer.y < buffer.scroll_top:
return
var param = params.get_param(0, 1)
for y in range(buffer.scroll_top, buffer.scroll_bottom + 1):
var line = buffer.lines.get_el(buffer.ybase + y)
line.delete_cells(0, param, buffer.get_null_cell(_erase_attr_data()),
_erase_attr_data())
line.is_wrapped = false
func cursor_up(params) -> void:
@ -464,15 +632,39 @@ func cursor_up(params) -> void:
_move_cursor(0, -params.get_param(0, 1))
func scroll_right(params):
print("TODO: scroll_right")
# CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48
#
# Notation: (Pn)
# Representation: CSI Pn 02/00 04/01
# Parameter default value: Pn = 1
# SR causes the data in the presentation component to be moved by n character positions
# if the line orientation is horizontal, or by n line positions if the line orientation
# is vertical, such that the data appear to move to the right; where n equals the value of Pn.
# The active presentation position is not affected by this control function.
#
# Supported:
# - always right shift (no line orientation setting respected)
#
# @vt: #Y CSI SR "Scroll Right" "CSI Ps SP A" "Scroll viewport `Ps` times to the right."
# SL moves the content of all lines within the scroll margins `Ps` times to the right.
# Content at the right margin is lost.
# SL has no effect outside of the scroll margins.
func scroll_right(params) -> void:
if buffer.y > buffer.scroll_bottom or buffer.y < buffer.scroll_top:
return
var param = params.get_param(0, 1)
for y in range(buffer.scroll_top, buffer.scroll_bottom + 1):
var line = buffer.lines.get_el(buffer.ybase + y)
line.insert_cells(0, param, buffer.get_null_cell(_erase_attr_data()),
_erase_attr_data())
line.is_wrapped = false
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))
_move_cursor(0, min(diff_to_bottom, params.get_param(0,1)))
else:
_move_cursor(0, params.get_param(0, 1))
@ -502,18 +694,18 @@ func cursor_char_absolute(params):
func cursor_position(params):
_set_cursor(
# col
(params.get_param(1, 1)) - 1 if params.size() >= 2 else 0,
(params.get_param(1, 1)) - 1 if params.length >= 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)
_set_cursor(params.get_param(0, 1) - 1, self._buffer.y)
func h_position_relative(params):
_move_cursor(params[0] if params[0] else 1, 0)
_move_cursor(params.get_param(0, 1), 0)
func line_pos_absolute(params):
@ -521,12 +713,13 @@ func line_pos_absolute(params):
func v_position_relative(params):
_move_cursor(0, params[0] if params[0] else 1)
_move_cursor(0, params.get_param(0, 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.
@ -537,7 +730,7 @@ func h_v_position(params):
# @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]:
match params.get_param(0):
3:
self._buffer.tabs = {}
0, _:
@ -551,7 +744,7 @@ func tab_clear(params) -> void:
func cursor_forward_tab(params) -> void:
if self._buffer.x >= self._buffer.cols:
return
var param = params[0] if params[0] else 1
var param = params.get_param(0, 1)
while param:
self._buffer.x = self._buffer.next_stop()
param -= 1
@ -560,7 +753,7 @@ func cursor_forward_tab(params) -> void:
func cursor_backward_tab(params) -> void:
if self._buffer.x >= _buffer_service.cols:
return
var param = params[0] if params[0] else 1
var param = params.get_param(0, 1)
while param:
self._buffer.x = self._buffer.buffer.prev_stop()
param -= 1
@ -582,10 +775,10 @@ func _erase_in_buffer_line(y: int, start: int, end: int, clear_wrap: bool = fals
# 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
# @param y 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()))
var line = buffer.lines.get_line(buffer.ybase + y)
line.fill(buffer.get_null_cell(_erase_attr_data()))
line.is_wrapped = false
@ -594,33 +787,32 @@ func erase_in_display(params) -> void:
var j
match params.get_param(0):
0:
j = self._buffer.y
j = buffer.y
# _dirty_row_service.mark_dirty(j)
_erase_in_buffer_line(j, self._buffer.x, _buffer_service.cols, self._buffer.x == 0)
_erase_in_buffer_line(j, buffer.x, _buffer_service.cols, 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
j = 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:
_erase_in_buffer_line(j, 0, buffer.x + 1, true)
if 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
buffer.lines.get_el(j + 1).is_wrapped = false
while j > 0:
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)
while j > 0:
j -= 1
_reset_buffer_line(j)
# _dirty_row_sevice.mark_dirty(0)
3:
# Clear scrollback (everything not in viewport)
@ -644,12 +836,71 @@ func erase_in_line(params):
_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")
# CSI Ps L
# Insert Ps Line(s) (default = 1) (IL).
#
# @vt: #Y CSI IL "Insert Line" "CSI Ps L" "Insert `Ps` blank lines at active row (default=1)."
# For every inserted line at the scroll top one line at the scroll bottom gets removed.
# The cursor is set to the first column.
# IL has no effect if the cursor is outside the scroll margins.
func insert_lines(params) -> void:
_restrict_cursor()
var param = params.get_param(0, 1)
if buffer.y > buffer.scroll_bottom or buffer.y < buffer.scroll_top:
return
var row: int = buffer.ybase + buffer.y
var scroll_bottom_row_offset = _buffer_service.rows - 1 - buffer.scroll_bottom
var scroll_bottom_absolute = _buffer_service.rows - 1 + buffer.ybase - scroll_bottom_row_offset + 1
while param:
# test: echo -e '\e[44m\e[1L\e[0m'
# blank_line(true) - xterm/linux behavior
buffer.lines.splice(scroll_bottom_absolute - 1, 1)
buffer.lines.splice(row, 0, [buffer.get_blank_line(_erase_attr_data())])
param -= 1
buffer.x = 0 # see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
# CSI Ps M
# Delete Ps Line(s) (default = 1) (DL).
#
# @vt: #Y CSI DL "Delete Line" "CSI Ps M" "Delete `Ps` lines at active row (default=1)."
# For every deleted line at the scroll top one blank line at the scroll bottom gets appended.
# The cursor is set to the first column.
# DL has no effect if the cursor is outside the scroll margins.
func delete_lines(params) -> void:
_restrict_cursor()
var param = params.get_param(0, 1)
if buffer.y > buffer.scroll_bottom or buffer.y < buffer.scroll_top:
return
var row: int = buffer.ybase + buffer.y
var j: int
j = _buffer_service.rows - 1 - buffer.scroll_bottom
j = _buffer_service.rows - 1 + buffer.ybase - j
while param:
# test echo -e '\e[44m\e[1M\e[0m'
# blank_line(true) - xterm/linux behavior
buffer.lines.splice(row, 1)
buffer.lines.splice(j, 0, [buffer.get_blank_line(_erase_attr_data())])
param -= 1
# CSI Ps P
# Delete Ps Character(s) (default = 1) (DCH).
#
# @vt: #Y CSI DCH "Delete Character" "CSI Ps P" "Delete `Ps` characters (default=1)."
# As characters are deleted, the remaining characters between the cursor and right margin move to the left.
# Character attributes move with the characters. The terminal adds blank characters at the right margin.
#
#
# FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)
func delete_chars(params) -> void:
_restrict_cursor()
var line = buffer.lines.get_el(buffer.ybase + buffer.y)
@ -659,10 +910,31 @@ func delete_chars(params) -> void:
#_dirty_row_service.markDirty(buffer.y)
func scroll_up(params):
print("TODO: scroll_up")
func scroll_down(params):
print("TODO: scroll_down")
# CSI Ps S Scroll up Ps lines (default = 1) (SU).
#
# @vt: #Y CSI SU "Scroll Up" "CSI Ps S" "Scroll `Ps` lines up (default=1)."
#
#
# FIXME: scrolled out lines at top = 1 should add to scrollback (xterm)
func scroll_up(params) -> void:
var param = params.get_param(0, 1)
while param:
buffer.lines.splice(buffer.ybase + buffer.scroll_top, 1)
buffer.lines.splice(buffer.ybase + buffer.scroll_bottom, 0,
[buffer.get_blank_line(_erase_attr_data())])
param -= 1
# CSI Ps T Scroll down Ps lines (default = 1) (SD).
#
# @vt: #Y CSI SD "Scroll Down" "CSI Ps T" "Scroll `Ps` lines down (default=1)."
func scroll_down(params) -> void:
var param = params.get_param(0, 1)
while param:
buffer.lines.splice(buffer.ybase + buffer.scroll_bottom, 1)
buffer.lines.splice(buffer.ybase + buffer.scroll_top, 0,
buffer.get_blank_line(DEFAULT_ATTRIBUTE_DATA))
param -= 1
func erase_chars(params) -> void:
@ -686,15 +958,19 @@ func repeat_preceding_character(params) -> void:
func send_device_attributes_primary(params):
print("TODO: send dev attr primary")
# TODO
pass
func send_device_attributes_secondary(params):
print("TODO: send dev attr second")
# TODO
pass
func set_mode(params):
print("TODO: set mode")
# TODO
pass
func reset_mode(params) -> void:
for param in params.params:
@ -708,14 +984,15 @@ func reset_mode(params) -> void:
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
if params.length == 1 and params.get_param(0) == 0:
_cur_attr_data.fg = AttributeData.new().fg
_cur_attr_data.bg = AttributeData.new().bg
return
var attr = _cur_attr_data
for p in params.to_array():
for i in range(params.length):
var p = params.get_param(i)
if p >= 30 and p <= 37:
# fg color 8
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK)
@ -745,17 +1022,140 @@ func char_attributes(params):
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)
_process_underline(params.get_sub_params(i)[0] if params.has_sub_params(i) else UnderlineStyle.SINGLE, attr)
elif p == 5:
# blink
# test with: echo -e '\e[5mblink\e[m'
attr.fg |= FgFlags.BLINK
elif p == 7:
# inverse and positive
# test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m'
attr.fg |= FgFlags.INVERSE
elif p == 8:
# invisible
attr.fg |= FgFlags.INVISIBLE
elif p == 2:
# dimmed text
attr.bg |= BgFlags.DIM
elif p == 21:
# double underline
_process_underline(UnderlineStyle.DOUBLE, attr)
elif p == 22:
# not bold nor faint
attr.fg &= ~FgFlags.BOLD
attr.bg &= ~BgFlags.DIM
elif p == 23:
# not italic
attr.bg &= ~BgFlags.ITALIC
elif p == 24:
# not underlined
attr.fg &= ~FgFlags.UNDERLINE
elif p == 25:
# not blink
attr.fg &= ~FgFlags.BLINK
elif p == 27:
# not inverse
attr.fg &= ~FgFlags.INVERSE
elif p == 28:
# not invisible
attr.fg &= ~FgFlags.INVISIBLE
elif p == 39:
# reset fg
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK)
attr.fg |= AttributeData.new().fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK)
elif p == 49:
# reset bg
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK)
attr.bg |= AttributeData.new().bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK)
elif p == 38 or p == 48 or p == 58:
# fg color 256 and RGB
i += _extract_color(params, i, attr)
elif p == 59:
attr.extended = attr.extended.duplicate()
attr.extended.underline_color = -1
attr.update_extended()
elif p == 100: # FIXME: dead branch, p=100 already handled above!
# TODO reset fg/bg
pass
func device_status(params):
print("TODO: dev stat")
# TODO
pass
func device_status_private(params):
print("TODO: dev stat priv")
# TODO
pass
# CSI ! p Soft terminal reset (DECSTR).
# http://vt100.net/docs/vt220-rm/table4-10.html
#
# @vt: #Y CSI DECSTR "Soft Terminal Reset" "CSI ! p" "Reset several terminal attributes to initial state."
# There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full terminal bootstrap,
# DECSTR only resets certain attributes. For most needs DECSTR should be sufficient.
#
# The following terminal attributes are reset to default values:
# - IRM is reset (dafault = false)
# - scroll margins are reset (default = viewport size)
# - erase attributes are reset to default
# - charsets are reset
# - DECSC data is reset to initial values
# - DECOM is reset to absolute mode
#
#
# FIXME: there are several more attributes missing (see VT520 manual)
func soft_reset(params):
print("TODO: soft reset")
func set_cursor_style(params):
print("TODO: set cur styl")
_core_service.is_cursor_hidden = false
emit_signal("scrollbar_sync_requested")
buffer.scroll_top = 0
buffer.scroll_bottom = _buffer_service.rows - 1
_cur_attr_data = DEFAULT_ATTRIBUTE_DATA
_core_service.reset()
_charset_service.reset()
# reset DECSC data
buffer.saved_x = 0
buffer.saved_y = buffer.ybase
buffer.saved_cur_attr_data.fg = _cur_attr_data.fg
buffer.saved_cur_attr_data.bg = _cur_attr_data.bg
buffer.saved_charset = _charset_service.charset
# reset DECOM
_core_service.dec_private_modes.origin = false
# CSI Ps SP q Set cursor style (DECSCUSR, VT520).
# Ps = 0 -> blinking block.
# Ps = 1 -> blinking block (default).
# Ps = 2 -> steady block.
# Ps = 3 -> blinking underline.
# Ps = 4 -> steady underline.
# Ps = 5 -> blinking bar (xterm).
# Ps = 6 -> steady bar (xterm).
#
# @vt: #Y CSI DECSCUSR "Set Cursor Style" "CSI Ps SP q" "Set cursor style."
# Supported cursor styles:
# - empty, 0 or 1: steady block
# - 2: blink block
# - 3: steady underline
# - 4: blink underline
# - 5: steady bar
# - 6: blink bar
func set_cursor_style(params) -> void:
var param = params.get_param(0, 1)
match param:
1, 2:
_options_service.options.cursor_style = CursorStyle.BLOCK
3, 4:
_options_service.options.cursor_style = CursorStyle.UNDERLINE
5, 6:
_options_service.options.cursor_style = CursorStyle.BAR
var is_blinking = param % 2 == 1
_options_service.options.cursor_blink = is_blinking
func set_scroll_region(params) -> void:
@ -794,11 +1194,44 @@ func window_options(params):
pass
# CSI Pm ' }
# Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.
#
# @vt: #Y CSI DECIC "Insert Columns" "CSI Ps ' }" "Insert `Ps` columns at cursor position."
# DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll margins,
# moving content to the right. Content at the right margin is lost.
# DECIC has no effect outside the scrolling margins.
func insert_columns(params):
print("TODO: insert_columns")
if buffer.y > buffer.scroll_bottom or buffer.y < buffer.scroll_top:
return
var param = params.get_param(0, 1)
for y in range(buffer.scroll_top, buffer.scroll_bottom + 1):
var line = buffer.lines.get_el(buffer.ybase + y)
line.insert_cells(buffer.x, param, buffer.get_null_cells(_erase_attr_data()),
_erase_attr_data())
line.is_wrapped = false
# CSI Pm ' ~
# Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.
#
# @vt: #Y CSI DECDC "Delete Columns" "CSI Ps ' ~" "Delete `Ps` columns at cursor position."
# DECDC deletes `Ps` times columns at the cursor position for all lines with the scroll margins,
# moving content to the left. Blank columns are added at the right margin.
# DECDC has no effect outside the scrolling margins.
func delete_columns(params):
print("TODO: delete_cols")
if buffer.y > buffer.scroll_bottom or buffer.y < buffer.scroll_top:
return
var param = params.get_param(0, 1)
for y in range(buffer.scroll_top, buffer.scroll_bottom + 1):
var line = buffer.lines.get_el(buffer.ybase + y)
line.delete_cells(buffer.x, param, buffer.get_null_cells(_erase_attr_data()),
_erase_attr_data())
line.is_wrapped = false
func set_mode_private(params) -> void:
@ -870,7 +1303,6 @@ func set_mode_private(params) -> void:
_core_service.dec_private_modes.bracketed_paste_mode = true
func reset_mode_private(params):
for param in params.to_array():
match param:
@ -942,7 +1374,7 @@ func _update_attr_color(color: int, mode: int, c1: int, c2: int, c3: int) -> int
# Helper to extract and apply color params/subparams.
# Returns advance for params index.
func _extract_color(params: Array, pos: int, attr) -> int:
func _extract_color(params, pos: int, attr) -> int:
# normalize params
# meaning: [target, CM, ign, val, val, val]
# RGB : [ 38/34, 2, ign, r, g, b]
@ -955,8 +1387,8 @@ func _extract_color(params: Array, pos: int, attr) -> int:
# 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]
while advance + pos < params.length and advance + c_space < accu.size():
accu[advance + c_space] = params.get_param(pos + advance)
advance += 1
# TODO FIX and FINISH me
return advance
@ -983,17 +1415,16 @@ func restore_cursor(params = null) -> void:
#
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
if buffer.y == buffer.scroll_top:
# possibly move the code below to term.reverse_srcoll()
# test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
# blank_line(true) is xterm/linux behavior
var scroll_region_height = buffer.scroll_bottom - buffer.scroll_top
buffer.lines.shift_elements(buffer.ybase + buffer.y, scroll_region_height, 1)
buffer.lines.set_line(buffer.ybase + buffer.y, buffer.get_blank_line(_erase_attr_data()))
else:
buffer.y -= 1
_restrict_cursor() # quickfix to not run out of bounds
# ESC c
@ -1012,7 +1443,7 @@ func reset() -> void:
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!
attr.extended = attr.extended.duplicate()
# default to 1 == single underline
if not ~style or style > 5:
@ -1034,3 +1465,27 @@ 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
# ESC # 8
# DEC mnemonic: DECALN (https://vt100.net/docs/vt510-rm/DECALN.html)
# This control function fills the complete screen area with
# a test pattern (E) used for adjusting screen alignment.
#
# @vt: #Y ESC DECALN "Screen Alignment Pattern" "ESC # 8" "Fill viewport with a test pattern (E)."
func screen_alignment_pattern() -> void:
# prepare cell data
var cell = CellData.new()
cell.content = 1 << Content.WIDTH_SHIFT | 'E'.ord_at(0)
cell.fg = _cur_attr_data.fg
cell.bg = _cur_attr_data.bg
_set_cursor(0, 0)
for y_offset in range(0, _buffer_service.rows):
var row = buffer.ybase + buffer.y + y_offset
var line = buffer.lines.get_line(row)
if line:
line.fill(cell)
line.is_wrapped = false
_set_cursor(0, 0)

View file

@ -145,8 +145,9 @@ func set_execute_handler_fallback(target: Object, method: String):
_execute_handler_fb = { 'target': target, 'method': method }
func set_esc_handler(id, target, method):
_esc_handlers[identifier(id, [0x30, 0x7e])] = [{'target': target, 'method': method}]
func set_esc_handler(id, target, method, arg = null):
_esc_handlers[identifier(id, [0x30, 0x7e])] = [{'target': target,
'method': method, 'arg': arg}]
func set_esc_handler_fallback(target: Object, method: String):
@ -229,6 +230,7 @@ func parse(data: Array, length: int):
ParserAction.EXECUTE:
var handler = _execute_handlers.get(code)
if handler:
print("EXEC: ", handler['method'])
handler['target'].call(handler['method'])
elif _execute_handler_fb:
_execute_handler_fb['target'].call(_execute_handler_fb['method'], code)
@ -243,6 +245,7 @@ func parse(data: Array, length: int):
var handlers = _csi_handlers.get((collect << 8 | code), [])
handlers.invert()
for handler in handlers:
print("CSI: ", handler['method'])
# undefined or true means success and to stop bubbling
if handler['target'].call(handler['method'], params):
continue
@ -277,6 +280,11 @@ func parse(data: Array, length: int):
handlers.invert()
for handler in handlers:
# undefined or true means success and to stop bubbling
print("ESC: ", handler['method'])
if handler['arg']:
if handler['target'].call(handler['method'], handler['arg']) != false:
continue
else:
if handler['target'].call(handler['method']) != false:
continue
handlers.invert()

View file

@ -75,6 +75,12 @@ func add_param(value: int):
params[length] = MAX_VALUE if value > MAX_VALUE else value
length += 1
# Add a sub parameter value.
# The sub parameter is automatically associated with the last parameter value.
# Thus it is not possible to add a subparameter without any parameter added yet.
# `Params` only stores up to `subParamsLength` sub parameters, any later
# sub parameter will be ignored.
func add_sub_param(value: int):
digit_is_sub = true
if !length:
@ -88,6 +94,21 @@ func add_sub_param(value: int):
sub_params_length += 1
sub_params_idx[length - 1] += 1
# Whether parameter at index `idx` has sub parameters.
func has_sub_params(idx: int) -> bool:
return (sub_params_idx[idx] & 0xFF) - (sub_params_idx[idx] >> 8) > 0
func get_sub_params(idx: int):
var start = sub_params_idx[idx] >> 8
var end = sub_params_idx[idx] & 0xFF
if end - start > 0:
return sub_params.slice(start, end - 1)
else:
return null
func add_digit(value: int):
var _length = sub_params_length if digit_is_sub else length
if _reject_digits or (not _length) or (digit_is_sub and _reject_sub_digits):
@ -97,10 +118,6 @@ func add_digit(value: int):
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):

View file

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

View file

@ -1,118 +0,0 @@
# Copyright (c) 2017 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends Reference
const CharacterJoinerRegistry = preload("res://addons/godot_xterm/renderer/character_joiner_registry.gd")
const TextRenderLayer = preload("res://addons/godot_xterm/renderer/text_render_layer.gd")
signal redraw_requested
signal options_changed
signal grid_changed(start, end)
var _id: int
var _render_layers: Array
var _device_pixel_ratio: float
var _character_joiner_registry
var _colors
var _container
var _buffer_service
var _options_service
var _char_size_service
var dimensions
func _init(colors, container: Node, buffer_service, options_service):
_id = get_instance_id()
_colors = colors
_container = container
_buffer_service = buffer_service
_options_service = options_service
var allow_transparency = _options_service.options.allow_transparency
_character_joiner_registry = CharacterJoinerRegistry.new(_buffer_service)
_render_layers = [
TextRenderLayer.new(_container, 0, _colors, _character_joiner_registry,
allow_transparency, _id, _buffer_service, _options_service)
]
# Connect render layers to our signals.
for layer in _render_layers:
self.connect("options_changed", layer, "on_options_changed")
self.connect("grid_changed", layer, "on_grid_changed")
dimensions = {
"scaled_char_width": 0,
"scaled_char_height": 0,
"scaled_cell_width": 0,
"scaled_cell_height": 0,
"scaled_char_left": 0,
"scaled_char_top": 0,
"scaled_canvas_width": 0,
"scaled_canvas_height": 0,
"canvas_width": 0,
"canvas_height": 0,
"actual_cell_width": 0,
"actual_cell_height": 0,
}
_device_pixel_ratio = OS.get_screen_dpi()
_update_dimensions()
emit_signal("options_changed")
func on_resize(cols, rows):
# Update character and canvas dimensions
_update_dimensions()
# Resize all render layers
for layer in _render_layers:
layer.resize(dimensions)
func refresh_rows(start: int, end: int) -> void:
emit_signal("grid_changed", start, end)
# Recalculates the character and canvas dimensions.
func _update_dimensions():
var char_width = 0
var char_height = 0
for font in _options_service.options.font_family.values():
var size = font.get_string_size("W")
char_width = max(char_width, size.x)
char_height = max(char_height, size.y)
dimensions.scaled_char_width = char_width
dimensions.scaled_char_height = char_height
# Calculate the scaled cell height, if line_height is not 1 then the value
# will be floored because since line_height can never be lower then 1, there
# is a guarantee that the scaled line height will always be larger than
# scaled char height.
dimensions.scaled_cell_height = floor(dimensions.scaled_char_height * _options_service.options.line_height)
# Calculate the y coordinate within a cell that text should draw from in
# order to draw in the center of a cell.
dimensions.scaled_char_top = 0 if _options_service.options.line_height == 1 else \
round((dimensions.scaled_cell_height - dimensions.scaled_char_height) / 2)
# Calculate the scaled cell width, taking the letter_spacing into account.
dimensions.scaled_cell_width = dimensions.scaled_char_width + round(_options_service.options.letter_spacing)
# Calculate the x coordinate with a cell that text should draw from in
# order to draw in the center of a cell.
dimensions.scaled_char_left = floor(_options_service.options.letter_spacing / 2)

View file

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

View file

@ -4,7 +4,7 @@
extends Reference
signal buffer_activated(active_buffer, inactive_buffer)
signal resized
signal resized(cols, rows)
const BufferSet = preload("res://addons/godot_xterm/buffer/buffer_set.gd")
@ -29,11 +29,25 @@ func _get_buffer():
func _init(options_service):
_options_service = options_service
_options_service.connect("option_changed", self, "_option_changed")
cols = max(_options_service.options.cols, MINIMUM_COLS)
rows = max(_options_service.options.rows, MINIMUM_ROWS)
buffers = BufferSet.new(_options_service, self)
buffers.connect("buffer_activated", self, "_buffer_activated")
func resize(cols: int, rows: int) -> void:
self.cols = cols
self.rows = rows
buffers.resize(cols, rows)
#buffers.setup_tab_stops(cols)
emit_signal("resized", cols, rows)
func _buffer_activated(active_buffer, inactive_buffer):
emit_signal("buffer_activated", active_buffer, inactive_buffer)
func _option_changed(option: String) -> void:
if option == "cols" or option == "rows":
resize(_options_service.options.cols, _options_service.options.rows)

View file

@ -4,45 +4,72 @@
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
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
const CursorStyle = Constants.CursorStyle
const UnderlineStyle = Constants.UnderlineStyle
const BellStyle = Constants.BellStyle
class TerminalOptions:
extends Reference
var cols: int = 80
var rows: int = 24
var cursor_blink: bool = false
var cursor_style = CursorStyle.BLOCK
# var cursor_width: int = 1
# var bell_sound: AudioStream = null
# var bell_style = BellStyle.NONE
# var draw_bold_text_in_bright_colors: bool = true
# var fast_scroll_modifier = "alt"
# var fast_scroll_sensitivity: int = 5
var font_family: Dictionary = {
# TODO
}
var font_size: int = 15
# var font_weight: String # TODO: Remove
# var font_weight_bold: String # TODO: Remove
var line_height: float = 1.0
# var link_tooltip_hover_duration: int # TODO: Remove
var letter_spacing: float = 0
# var log_level # TODO: implement
var scrollback: int = 1000
# var scroll_sensitivity: int = 1
var screen_reader_mode: bool = false
# var mac_option_is_meta: bool = false
# var mac_option_click_forces_selection: bool = false
# var minimum_contrast_ratio: float = 1
# var disable_stdin: bool = false
# var allow_proposed_api: bool = true
var allow_transparency: bool = false
var tab_stop_width: int = 8
# var colors: Dictionary = {
# 'black': Color(0, 0, 0)
# }
# var right_click_selects_word = "isMac" # TODO?
# var renderer_type = "canvas" # Remove?
var window_options: Dictionary = {
'set_win_lines': false,
}
var windows_mode: bool = false
# var word_separator: String = " ()[]{}',\""
var convert_eol: bool = true
# var term_name: String = "xterm"
# var cancel_events: bool = false
# Copies options from an `object` to itself.
func copy_from(object: Object):
for property in get_property_list():
if property.usage == PROPERTY_USAGE_SCRIPT_VARIABLE:
var p = object.get(property.name)
if p:
set(property.name, p)
var DEFAULT_OPTIONS = TerminalOptions.new()
signal option_changed
@ -51,3 +78,27 @@ var options
func _init(options):
self.options = options
# Set the font size based on the font_size option
_resize_fonts()
func set_option(key: String, value) -> void:
# TODO: sanitize and validate options.
# Don't fire an option change event if they didn't change
if options[key] == value:
return
options[key] = value
emit_signal("option_changed", key)
# Update other options accordingly.
match key:
"font_size":
_resize_fonts()
func _resize_fonts():
for font in options.font_family.values():
font.size = options.font_size

View file

@ -33,9 +33,9 @@ const Const = preload("res://addons/godot_xterm/Constants.gd")
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
const Parser = preload("res://addons/godot_xterm/parser/escape_sequence_parser.gd")
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
const 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 AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.gd")
const SourceCodeProRegular = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres")
const SourceCodeProBold = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres")
@ -52,15 +52,21 @@ const LEFT_BRACKET = 91
const ENTER = 10
const BACKSPACE_ALT = 127
const BLINK_INTERVAL = 0.6 # 600ms. The time between blinks.
# TODO: Move me somewhere else.
enum BellStyle {
NONE
}
signal output(data)
signal scrolled(ydisp)
export var cols = 80
export var rows = 24
# If set, terminals rows and cols will be automatically calculated based on the
# control's rect size and font_size.
export var auto_resize = false
export var cursor_blink = false
export var cursor_style = 'block'
export var cursor_width = 1
@ -76,8 +82,6 @@ export var font_family: Dictionary = {
"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
@ -93,7 +97,22 @@ 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)
"black": Color("#2e3436"),
"red": Color("#cc0000"),
"green": Color("#4e9a06"),
"yellow": Color("#c4a000"),
"blue": Color("#3465a4"),
"magenta": Color("#75507b"),
"cyan": Color("#06989a"),
"white": Color("#d3d7cf"),
"bright_black": Color("#555753"),
"bright_red": Color("#ef2929"),
"bright_green": Color("#8ae234"),
"bright_yellow": Color("#fce94f"),
"bright_blue": Color("#729fcf"),
"bright_magenta": Color("#ad7fa8"),
"bright_cyan": Color("#34e2e2"),
"bright_white": Color("#eeeeec"),
}
export var right_click_selects_word = 'isMac' # TODO
export var renderer_type = 'canvas' # Relevant?
@ -122,19 +141,13 @@ var _scaled_cell_height
var _scaled_char_top
var _scaled_char_left
var _work_cell = CellData.new()
var _blink_on = false
var _time_since_last_blink = 0
func _ready():
var options = OptionsService.TerminalOptions.new()
options.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
options.copy_from(self)
options_service = OptionsService.new(options)
options_service.connect("option_changed", self, "_update_options")
_buffer_service = BufferService.new(options_service)
_core_service = CoreService.new()
@ -151,9 +164,10 @@ func _ready():
_color_manager = ColorManager.new()
_color_manager.set_theme(colors)
_render_service = Renderer.new(_color_manager.colors, self, _buffer_service, options_service)
if auto_resize:
connect("resized", self, "_update_dimensions")
_update_dimensions()
@ -232,20 +246,145 @@ func _update_dimensions():
# order to draw in the center of a cell.
_scaled_char_left = floor(options_service.options.letter_spacing / 2)
if auto_resize:
# Calculate cols and rows based on cell size
var rect: Rect2 = get_rect()
var cols = max(2, floor(rect.size.x / _scaled_cell_width))
var rows = max(1, floor(rect.size.y / _scaled_cell_height))
self.cols = cols
self.rows = rows
options_service.set_option("rows", rows)
options_service.set_option("cols", cols)
# Scroll the terminal down 1 row, creating a blank line.
# @param is_wrapped Whether the new line is wrapped from the previous line.
func scroll(erase_attr, is_wrapped: bool = false) -> void:
var buffer = _buffer_service.buffer
var new_line = buffer.get_blank_line(erase_attr, is_wrapped)
var top_row = buffer.ybase + buffer.scroll_top
var bottom_row = buffer.ybase + buffer.scroll_bottom
if buffer.scroll_top == 0:
# Determine whether the buffer is going to be trimmed after insertion.
var will_buffer_be_trimmed = buffer.lines.is_full
# Insert the line using the fastest method
if bottom_row == buffer.lines.length - 1:
if will_buffer_be_trimmed:
buffer.lines.recycle().copy_from(new_line.duplicate())
else:
buffer.lines.push(new_line.duplicate())
else:
buffer.lines.splice(bottom_row + 1, 0, [new_line.duplicate()])
# Only adjust ybase and ydisp when the buffer is not trimmed
if not will_buffer_be_trimmed:
buffer.ybase += 1
# Only scroll the ydisp with ybase if the user has not scrolled up
if not _buffer_service.is_user_scrolling:
buffer.ydisp += 1
else:
# When the buffer is full and the user has scrolled up, keep the text
# stable unless ydisp is right at the top
if _buffer_service.is_user_scrolling:
buffer.ydisp = max(buffer.ydisp - 1, 0)
else:
# scroll_top is non-zero which means no line will be going to the
# scrollback, instead we can just shift them in-place.
var scroll_region_height = bottom_row - top_row + 1 # as it's zero based
buffer.lines.shift_elements(top_row + 1, scroll_region_height - 1, -1)
buffer.lines.set_line(bottom_row, new_line.duplicate())
# Move the viewport to the bottom of the buffer unless the user is scrolling.
if not _buffer_service.is_user_scrolling:
buffer.ydisp = buffer.ybase
# Flag rows that need updating
# TODO
emit_signal("scrolled", buffer.ydisp)
func _process(delta):
_time_since_last_blink += delta
if _time_since_last_blink > BLINK_INTERVAL:
_blink_on = not _blink_on
_time_since_last_blink = 0
update()
func _draw():
# Draw the background and foreground
if _buffer_service == null:
return
var buffer = _buffer_service.buffer
for y in range(buffer.ybase, rows):
var line = buffer.lines.get_el(y)
var rows = _buffer_service.rows
for y in range(0, rows):
var row = y + buffer.ydisp
var line = buffer.lines.get_line(row)
for x in line.length:
line.load_cell(x, _work_cell)
# Background
# Get the background color
# TODO: handle inverse
var bg_color
if _work_cell.is_bg_rgb():
bg_color = AttributeData.to_color_rgb(_work_cell.get_bg_color())
elif _work_cell.is_bg_palette():
bg_color = _color_manager.colors.ansi[_work_cell.get_bg_color()]
else:
bg_color = _color_manager.colors.background
draw_rect(Rect2(x * _scaled_cell_width, y * _scaled_cell_height,
(cols - x) * _scaled_cell_width, 1 * _scaled_cell_height), 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,
(cols - x) * _scaled_cell_width, 1 * _scaled_cell_height),
bg_color)
# Foreground
# Don't draw if cell is invisible
if _work_cell.is_invisible():
continue
# Don't draw if cell is blink and blink is off
if _work_cell.is_blink() and not _blink_on:
continue
# Get the foreground color
# TODO: handle inverse min contrast and draw bold in bright colors
# dim and maybe more!
var fg_color
if _work_cell.is_fg_default():
fg_color = _color_manager.colors.foreground
if _work_cell.is_fg_rgb():
fg_color = AttributeData.to_color_rgb(_work_cell.get_fg_color())
else:
fg_color = _color_manager.colors.ansi[_work_cell.get_fg_color()]
# Get font
var font: DynamicFont = options_service.options.font_family.regular
var is_bold = _work_cell.is_bold()
var is_italic = _work_cell.is_italic()
if is_bold and is_italic:
font = options_service.options.font_family.bold_italic
elif is_bold:
font = options_service.options.font_family.bold
elif is_italic:
font = options_service.options.font_family.italic
# TODO: set this once initially
font.size = options_service.options.font_size
draw_char(font,
Vector2(x * _scaled_cell_width + _scaled_char_left,
y * _scaled_cell_height + _scaled_char_top + _scaled_char_height / 2),
_work_cell.get_chars() if _work_cell.get_chars() else ' ', "", color)
_work_cell.get_chars() if _work_cell.get_chars() else ' ', "", fg_color)
# Draw the cursor
# Draw selection

61
demo.cast Normal file
View file

@ -0,0 +1,61 @@
{"version": 2, "width": 86, "height": 29, "timestamp": 1589772748, "env": {"SHELL": "/run/current-system/sw/bin/bash", "TERM": "xterm"}}
[0.082961, "o", "> "]
[0.798002, "o", "e"]
[0.893414, "o", "c"]
[0.956255, "o", "h"]
[1.008677, "o", "o"]
[1.089472, "o", " "]
[1.189602, "o", "h"]
[1.266892, "o", "e"]
[1.347483, "o", "l"]
[1.46568, "o", "l"]
[1.541039, "o", "o"]
[1.726772, "o", "\r\n"]
[1.727475, "o", "hello\r\n> "]
[2.060109, "o", "#"]
[2.179668, "o", " "]
[2.471941, "o", "T"]
[2.652735, "o", "h"]
[2.746515, "o", "i"]
[2.810578, "o", "s"]
[2.921342, "o", " "]
[2.98886, "o", "i"]
[3.069095, "o", "s"]
[3.31728, "o", " "]
[3.399615, "o", "a"]
[3.513605, "o", " "]
[3.72609, "o", "d"]
[3.811197, "o", "e"]
[3.94649, "o", "m"]
[4.047162, "o", "o"]
[4.225042, "o", "\r\n"]
[4.225402, "o", "> "]
[4.935288, "o", "t"]
[5.163552, "o", "o"]
[5.323205, "o", "i"]
[5.46746, "o", "l"]
[5.561098, "o", "et "]
[6.064937, "o", "-"]
[6.41563, "o", "-"]
[6.60443, "o", "g"]
[6.666621, "o", "a"]
[6.768317, "o", "y"]
[6.848917, "o", " "]
[7.076406, "o", "H"]
[7.250067, "o", "E"]
[7.410878, "o", "L"]
[7.537016, "o", "L"]
[7.604155, "o", "O"]
[7.888992, "o", " "]
[8.193437, "o", "W"]
[8.365871, "o", "O"]
[8.454678, "o", "R"]
[8.525163, "o", "L"]
[8.60286, "o", "D"]
[8.873053, "o", "!"]
[9.216434, "o", "\r\n"]
[9.251462, "o", " \r\n \u001b[0;1;31;91mm\u001b[0m \u001b[0;1;36;96mm\u001b[0m \u001b[0;1;34;94mmm\u001b[0;1;35;95mmm\u001b[0;1;31;91mmm\u001b[0m \u001b[0;1;33;93mm\u001b[0m \u001b[0;1;35;95mm\u001b[0m \u001b[0;1;36;96mmm\u001b[0;1;34;94mmm\u001b[0m \u001b[0;1;36;96mm\u001b[0m \u001b[0;1;31;91mm\u001b[0m \u001b[0;1;33;93mm\u001b[0;1;32;92mmm\u001b[0;1;36;96mm\u001b[0m \u001b[0;1;34;94mm\u001b[0;1;35;95mmm\u001b[0;1;31;91mmm\u001b[0m \u001b[0;1;32;92mm\u001b[0m \u001b[0;1;35;95mm\u001b[0;1;31;91mmm\u001b[0;1;33;93mm\u001b[0m \r\n \u001b[0;1;33;93m#\u001b[0m \u001b[0;1;34;94m#\u001b[0m \u001b[0;1;35;95m#\u001b[0m \u001b[0;1;32;92m#\u001b[0m \u001b[0;1;31;91m#\u001b[0m \u001b[0;1;36;96mm\u001b[0;1;34;94m\"\u001b[0m \u001b[0;1;35;95m\"\u001b[0;1;31;91mm\u001b[0m \u001b[0;1;34;94m#\u001b[0m \u001b[0;1;35;95m#\u001b[0m \u001b[0;1;33;93m#\u001b[0m \u001b[0;1;32;92mm\"\u001b[0m \u001b[0;1;34;94m\"m\u001b[0m \u001b[0;1;35;95m#\u001b[0m \u001b[0;1;33;93m\"\u001b[0;1;32;92m#\u001b[0m \u001b[0;1;36;96m#\u001b[0m \u001b[0;1;31;91m#\u001b[0m \u001b[0;1;32;92m\"\u001b[0;1;36;96mm\u001b[0m\r\n \u001b[0;1;32;92m#\u001b[0;1;36;96mmm\u001b[0;1;34;94mmm\u001b[0;1;35;95m#\u001b[0m \u001b[0;1;31;91m#m\u001b[0;1;33;93mmm\u001b[0;1;32;92mmm\u001b[0m \u001b[0;1;36;96m#\u001b[0m \u001b[0;1;33;93m#\u001b[0m \u001b[0;1;34;94m#\u001b"]
[9.251901, "o", "[0m \u001b[0;1;33;93m#\u001b[0m \u001b[0;1;35;95m\"\u001b[0m \u001b[0;1;31;91m#\"\u001b[0;1;33;93m#\u001b[0m \u001b[0;1;32;92m#\u001b[0m \u001b[0;1;36;96m#\u001b[0m \u001b[0;1;35;95m#\u001b[0m \u001b[0;1;31;91m#\u001b[0;1;33;93mmm\u001b[0;1;32;92mmm\u001b[0;1;36;96m\"\u001b[0m \u001b[0;1;34;94m#\u001b[0m \u001b[0;1;33;93m#\u001b[0m \u001b[0;1;34;94m#\u001b[0m\r\n \u001b[0;1;36;96m#\u001b[0m \u001b[0;1;31;91m#\u001b[0m \u001b[0;1;33;93m#\u001b[0m \u001b[0;1;34;94m#\u001b[0m \u001b[0;1;32;92m#\u001b[0m \u001b[0;1;35;95m#\u001b[0m \u001b[0;1;32;92m#\u001b[0m \u001b[0;1;31;91m#\u001b[0;1;33;93m#\u001b[0m \u001b[0;1;32;92m##\u001b[0;1;36;96m\"\u001b[0m \u001b[0;1;34;94m#\u001b[0m \u001b[0;1;31;91m#\u001b[0m \u001b[0;1;33;93m#\u001b[0m \u001b[0;1;36;96m\"\u001b[0;1;34;94mm\u001b[0m \u001b[0;1;35;95m#\u001b[0m \u001b[0;1;32;92m#\u001b[0m \u001b[0;1;35;95m#\u001b[0m\r\n \u001b[0;1;34;94m#\u001b[0m \u001b[0;1;33;93m#\u001b[0m \u001b[0;1;32;92m#m\u001b[0;1;36;96mmm\u001b[0;1;34;94mmm\u001b[0m \u001b[0;1;35;95m#\u001b[0;1;31;91mmm\u001b[0;1;33;93mmm\u001b[0;1;32;92mm\u001b[0m \u001b[0;1;36;96m#m\u001b[0;1;34;94mmm\u001b[0;1;35;95mmm\u001b[0m \u001b[0;1;33;93m#m\u001b[0;1;32;92mm#\u001b[0m \u001b[0;1;33;93m#\u001b[0m \u001b[0;1;36;96m#\u001b[0m \u001b[0;1;35;95m#\u001b[0;1;31;91mmm\u001b[0;1;33;93m#\u001b[0m \u001b[0;1;32;92m#\u001b[0m \u001b[0;1;35;95m\"\u001b[0m \u001b[0;1;31;91m#m\u001b[0;1;33;93mmm\u001b[0;1"]
[9.251944, "o", ";32;92mmm\u001b[0m \u001b[0;1;36;96m#\u001b[0;1;34;94mmm\u001b[0;1;35;95mm\"\u001b[0m \r\n \r\n \r\n \r\n \u001b[0;1;36;96mm\u001b[0m \r\n \u001b[0;1;34;94m#\u001b[0m \r\n \u001b[0;1;35;95m#\u001b[0m \r\n \u001b[0;1;31;91m\"\u001b[0m \r\n \u001b[0;1;33;93m#\u001b[0m \r\n \r\n \r\n"]
[9.252259, "o", "> "]
[12.56287, "o", "exit\r\n"]

View file

@ -38,7 +38,6 @@ func _ready():
if err != OK:
OS.alert("Couldn't connect to socat on %s:%d" % [host, port], "Connection Failed!")
var status = stream_peer.get_status()
var connected = stream_peer.is_connected_to_host()
@ -52,6 +51,13 @@ func _ready():
$Terminal.connect('output', self, 'send_data')
connect("data_received", $Terminal, "write")
connect("resized", self, "_resize_terminal")
_resize_terminal()
func _resize_terminal():
$Terminal.rect_size = OS.window_size
func send_data(data: PoolByteArray):
if record:

View file

@ -8,6 +8,7 @@
[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"]
show_behind_parent = true
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
@ -16,25 +17,39 @@ __meta__ = {
}
[node name="Terminal" type="Control" parent="."]
margin_left = 95.937
margin_top = 44.6138
margin_right = 695.937
margin_bottom = 444.614
anchor_right = 1.0
anchor_bottom = 1.0
rect_min_size = Vector2( 600, 400 )
script = ExtResource( 2 )
__meta__ = {
"_edit_use_anchors_": false
}
auto_resize = true
font_family = {
"bold": ExtResource( 6 ),
"bold_italic": ExtResource( 5 ),
"italic": ExtResource( 4 ),
"regular": ExtResource( 3 )
}
font_size = 16
line_height = 1.15
colors = {
"black": Color( 0.121569, 0.00784314, 0.00784314, 1 )
"black": Color( 0.180392, 0.203922, 0.211765, 1 ),
"blue": Color( 0.203922, 0.396078, 0.643137, 1 ),
"bright_black": Color( 0.333333, 0.341176, 0.32549, 1 ),
"bright_blue": Color( 0.447059, 0.623529, 0.811765, 1 ),
"bright_cyan": Color( 0.203922, 0.886275, 0.886275, 1 ),
"bright_green": Color( 0.541176, 0.886275, 0.203922, 1 ),
"bright_magenta": Color( 0.678431, 0.498039, 0.658824, 1 ),
"bright_red": Color( 0.937255, 0.160784, 0.160784, 1 ),
"bright_white": Color( 0.933333, 0.933333, 0.92549, 1 ),
"bright_yellow": Color( 0.988235, 0.913725, 0.309804, 1 ),
"cyan": Color( 0.0235294, 0.596078, 0.603922, 1 ),
"green": Color( 0.305882, 0.603922, 0.0235294, 1 ),
"magenta": Color( 0.458824, 0.313726, 0.482353, 1 ),
"red": Color( 0.8, 0, 0, 1 ),
"white": Color( 0.827451, 0.843137, 0.811765, 1 ),
"yellow": Color( 0.768627, 0.627451, 0, 1 )
}
window_options = {
"set_win_lines": false
}

View file

@ -100,9 +100,3 @@ func test_csi_position_cursor():
parse(parser, '\u001b[1;5H')
assert_eq(buffer.calls, [['csi', [1, 5]]])
assert_eq(buffer.printed, '')

View file

@ -124,5 +124,239 @@ class TestGetWrappedRangeForLine:
assert_eq(buffer.get_wrapped_range_for_line(buffer.lines.length - 2).last, INIT_ROWS - 1)
class TestResize:
extends BaseBufferTest
func before_each():
.before_each()
buffer.fill_viewport_rows()
func test_column_size_reduction_trims_data_in_the_buffer():
buffer.resize(INIT_COLS / 2, INIT_ROWS)
assert_eq(buffer.lines.length, INIT_ROWS)
for i in range(INIT_ROWS):
assert_eq(buffer.lines.get_line(i).length, INIT_COLS / 2)
func test_column_size_increase_adds_pad_columns():
buffer.resize(INIT_COLS + 10, INIT_ROWS)
assert_eq(buffer.lines.length, INIT_ROWS)
for i in range(INIT_ROWS):
assert_eq(buffer.lines.get_line(i).length, INIT_COLS + 10)
func test_row_size_reduction_trims_blank_lines_from_the_end():
buffer.resize(INIT_COLS, INIT_ROWS - 10)
assert_eq(buffer.lines.length, INIT_ROWS - 10)
func test_row_size_reduction_moves_viewport_down_when_it_is_at_the_end():
# Set cursor y to have 5 blank lines below it
buffer.y = INIT_ROWS - 5 - 1
buffer.resize(INIT_COLS, INIT_ROWS - 10)
# Trim 5 rows
assert_eq(buffer.lines.length, INIT_ROWS - 5)
# Shift the viewport down 5 rows
assert_eq(buffer.ydisp, 5)
assert_eq(buffer.ybase, 5)
func test_no_scrollback_trims_from_the_top_of_the_buffer_when_the_cursor_reaches_the_bottom():
buffer = Buffer.new(true, TestUtils.MockOptionsService.new({"scrollback": 0}), buffer_service)
assert_eq(buffer.lines.max_length, INIT_ROWS)
buffer.y = INIT_ROWS - 1
buffer.fill_viewport_rows()
var ch_data = buffer.lines.get_line(5).load_cell(0, CellData.new()).get_as_char_data()
ch_data[1] = "a"
buffer.lines.get_line(5).set_cell(0, CellData.from_char_data(ch_data))
ch_data = buffer.lines.get_line(INIT_ROWS - 1).load_cell(0, CellData.new()).get_as_char_data()
ch_data[1] = "b"
buffer.lines.get_line(INIT_ROWS - 1).set_cell(0, CellData.from_char_data(ch_data))
buffer.resize(INIT_COLS, INIT_ROWS - 5)
assert_eq(buffer.lines.get_line(0).load_cell(0, CellData.new()).get_as_char_data()[1], "a")
assert_eq(buffer.lines.get_line(INIT_ROWS - 1 - 5).load_cell(0, CellData.new()).get_as_char_data()[1], "b")
func test_row_size_increase_adds_blank_lines_to_empty_buffer():
assert_eq(buffer.ydisp, 0)
buffer.resize(INIT_COLS, INIT_ROWS + 10)
assert_eq(buffer.ydisp, 0)
assert_eq(buffer.lines.length, INIT_ROWS + 10)
func test_row_size_increase_shows_more_of_the_buffer_above():
# Create 10 extra blank lines
for i in range(10):
buffer.lines.push(buffer.get_blank_line(AttributeData.new()))
# Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1
# Scroll down 10 lines
buffer.ybase = 10
buffer.ydisp = 10
assert_eq(buffer.lines.length, INIT_ROWS + 10)
buffer.resize(INIT_COLS, INIT_ROWS + 5)
# Should be 5 more lines
assert_eq(buffer.ydisp, 5)
assert_eq(buffer.ybase, 5)
# Should not trim the buffer
assert_eq(buffer.lines.length, INIT_ROWS + 10)
func test_row_size_increase_shows_more_of_the_buffer_below_when_the_viewort_is_at_the_top_of_the_buffer():
# Create 10 extra blank lines
for i in range(10):
buffer.lines.push(buffer.get_blank_line(AttributeData.new()))
# Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1
# Scroll down 10 lines
buffer.ybase = 10
buffer.ydisp = 0
assert_eq(buffer.lines.length, INIT_ROWS + 10)
buffer.resize(INIT_COLS, INIT_ROWS + 5)
# The viewport should remain at the top
assert_eq(buffer.ydisp, 0)
# The buffer ybase should move up 5 lines
assert_eq(buffer.ybase, 5)
# Should not trim the buffer
assert_eq(buffer.lines.length, INIT_ROWS + 10)
func test_row_and_column_increase_resizes_properly():
buffer.resize(INIT_COLS + 5, INIT_ROWS + 5)
assert_eq(buffer.lines.length, INIT_ROWS + 5)
buffer.resize(INIT_COLS - 5, INIT_ROWS)
assert_eq(buffer.lines.length, INIT_ROWS)
func test_reflow_does_not_wrap_empty_lines():
assert_eq(buffer.lines.length, INIT_ROWS)
buffer.resize(INIT_COLS - 5, INIT_ROWS)
assert_eq(buffer.lines.length, INIT_ROWS)
func test_reflow_shrinks_row_length():
buffer.resize(5, 10)
assert_eq(buffer.lines.length, 10)
for i in range(10):
assert_eq(buffer.lines.get_line(i).length, 5)
func test_reflow_wraps_and_unwraps_lines():
buffer.resize(5, 10)
var first_line = buffer.lines.get_line(0)
for i in range(5):
var code = "a".ord_at(0) + i
var ch = char(code)
first_line.set_cell(i, CellData.from_char_data([0, ch, 1, code]))
buffer.y = 1
assert_eq(buffer.lines.get_line(0).length, 5)
assert_eq(buffer.lines.get_line(0).translate_to_string(), "abcde")
buffer.resize(1, 10)
assert_eq(buffer.lines.length, 10)
assert_eq(buffer.lines.get_line(0).translate_to_string(), "a")
assert_eq(buffer.lines.get_line(1).translate_to_string(), "b")
assert_eq(buffer.lines.get_line(2).translate_to_string(), "c")
assert_eq(buffer.lines.get_line(3).translate_to_string(), "d")
assert_eq(buffer.lines.get_line(4).translate_to_string(), "e")
assert_eq(buffer.lines.get_line(5).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(6).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(7).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(8).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(9).translate_to_string(), " ")
buffer.resize(5, 10)
assert_eq(buffer.lines.length, 10)
assert_eq(buffer.lines.get_line(0).translate_to_string(), "abcde")
assert_eq(buffer.lines.get_line(1).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(2).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(3).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(4).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(5).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(6).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(7).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(8).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(9).translate_to_string(), " ")
func test_discards_parts_of_wrapped_lines_that_go_out_of_the_scrollback():
options_service.options.scrollback = 1
buffer.resize(10, 5)
var last_line = buffer.lines.get_line(3)
for i in range(10):
var code = "a".ord_at(0) + i
var ch = char(code)
last_line.set_cell(i, CellData.from_char_data([0, ch, 1, code]))
assert_eq(buffer.lines.length, 5)
buffer.y = 4
buffer.resize(2, 5)
assert_eq(buffer.y, 4)
assert_eq(buffer.ybase, 1)
assert_eq(buffer.lines.get_line(0).translate_to_string(), "ab")
assert_eq(buffer.lines.get_line(1).translate_to_string(), "cd")
assert_eq(buffer.lines.get_line(2).translate_to_string(), "ef")
assert_eq(buffer.lines.get_line(3).translate_to_string(), "gh")
assert_eq(buffer.lines.get_line(4).translate_to_string(), "ij")
assert_eq(buffer.lines.get_line(5).translate_to_string(), " ")
buffer.resize(1, 5)
assert_eq(buffer.y, 4)
assert_eq(buffer.ybase, 1)
assert_eq(buffer.lines.length, 6)
assert_eq(buffer.lines.get_line(0).translate_to_string(), "f")
assert_eq(buffer.lines.get_line(1).translate_to_string(), "g")
assert_eq(buffer.lines.get_line(2).translate_to_string(), "h")
assert_eq(buffer.lines.get_line(3).translate_to_string(), "i")
assert_eq(buffer.lines.get_line(4).translate_to_string(), "j")
assert_eq(buffer.lines.get_line(5).translate_to_string(), " ")
buffer.resize(10, 5)
assert_eq(buffer.y, 1)
assert_eq(buffer.ybase, 0)
assert_eq(buffer.lines.length, 5)
assert_eq(buffer.lines.get_line(0).translate_to_string(), "fghij ")
assert_eq(buffer.lines.get_line(1).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(2).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(3).translate_to_string(), " ")
assert_eq(buffer.lines.get_line(4).translate_to_string(), " ")
func test_removes_the_correct_amount_of_rows_when_reflowing_larger():
# This is a regression test to ensure that successive wrapped lines that are getting
# 3+ lines removed on a reflow actually remove the right lines
buffer.resize(10, 10)
buffer.y = 2
var first_line = buffer.lines.get_line(0)
var second_line = buffer.lines.get_line(1)
for i in range(10):
var code = "a".ord_at(0) + i
var ch = char(code)
first_line.set_cell(i, CellData.from_char_data([0, ch, 1, code]))
for i in range(10):
var code = "0".ord_at(0) + i
var ch = char(code)
second_line.set_cell(i, CellData.from_char_data([0, ch, 1, code]))
assert_eq(buffer.lines.length, 10)
assert_eq(buffer.lines.get_line(0).translate_to_string(), "abcdefghij")
assert_eq(buffer.lines.get_line(1).translate_to_string(), "0123456789")
for i in range(2, 10):
assert_eq(buffer.lines.get_line(i).translate_to_string(), " ")
buffer.resize(2, 10)
assert_eq(buffer.ybase, 1)
assert_eq(buffer.lines.length, 11)
assert_eq(buffer.lines.get_line(0).translate_to_string(), "ab")
assert_eq(buffer.lines.get_line(1).translate_to_string(), "cd")
assert_eq(buffer.lines.get_line(2).translate_to_string(), "ef")
assert_eq(buffer.lines.get_line(3).translate_to_string(), "gh")
assert_eq(buffer.lines.get_line(4).translate_to_string(), "ij")
assert_eq(buffer.lines.get_line(5).translate_to_string(), "01")
assert_eq(buffer.lines.get_line(6).translate_to_string(), "23")
assert_eq(buffer.lines.get_line(7).translate_to_string(), "45")
assert_eq(buffer.lines.get_line(8).translate_to_string(), "67")
assert_eq(buffer.lines.get_line(9).translate_to_string(), "89")
assert_eq(buffer.lines.get_line(10).translate_to_string(), " ")
buffer.resize(10, 10)
assert_eq(buffer.ybase, 0)
assert_eq(buffer.lines.length, 10)
assert_eq(buffer.lines.get_line(0).translate_to_string(), "abcdefghij")
assert_eq(buffer.lines.get_line(1).translate_to_string(), "0123456789")
for i in range(2, 10):
assert_eq(buffer.lines.get_line(i).translate_to_string(), " ",
"line %d is incorrect" % i)

View file

@ -146,7 +146,6 @@ class TestCellData:
cell = CellData.new()
func test_char_data_cell_data_equality():
# ASCII
cell.set_from_char_data([123, 'a', 1, 'a'.ord_at(0)])
@ -243,7 +242,96 @@ class TestBufferLine:
[5, 'e', 0, 'e'.ord_at(0)],
])
# Skipped a bunch of tests here...
func test_copy_from():
var line = BufferLineTest.new(5)
line.set_cell(0, CellData.from_char_data([1, 'a', 0, 'a'.ord_at(0)]))
line.set_cell(0, CellData.from_char_data([2, 'b', 0, 'b'.ord_at(0)]))
line.set_cell(0, CellData.from_char_data([3, 'c', 0, 'c'.ord_at(0)]))
line.set_cell(0, CellData.from_char_data([4, 'd', 0, 'd'.ord_at(0)]))
line.set_cell(0, CellData.from_char_data([5, 'e', 0, 'e'.ord_at(0)]))
var line2 = BufferLineTest.new(5, CellData.from_char_data([1, 'a', 0, 'a'.ord_at(0)]), true)
line2.copy_from(line)
assert_eq(line2.to_array(), line.to_array())
assert_eq(line2.length, line.length)
assert_eq(line2.is_wrapped, line.is_wrapped)
class TestResize:
extends "res://addons/gut/test.gd"
var CHAR_DATA = [1, 'a', 0, 'a'.ord_at(0)]
var line
func repeat(el, times: int) -> Array:
var result = []
result.resize(times)
for i in range(times):
result[i] = el
return result
func test_enlarge():
line = BufferLineTest.new(5, CellData.from_char_data(CHAR_DATA), false)
line.resize(10, CellData.from_char_data(CHAR_DATA))
assert_eq(line.to_array(), repeat(CHAR_DATA, 10))
func test_shrink():
line = BufferLineTest.new(10, CellData.from_char_data(CHAR_DATA), false)
line.resize(5, CellData.from_char_data(CHAR_DATA))
assert_eq(line.to_array(), repeat(CHAR_DATA, 5))
func test_shrink_to_0_length():
line = BufferLineTest.new(10, CellData.from_char_data(CHAR_DATA), false)
line.resize(0, CellData.from_char_data(CHAR_DATA))
assert_eq(line.to_array(), repeat(CHAR_DATA, 0))
func shrink_then_enlarge():
line = BufferLineTest.new(10, CellData.from_char_data(CHAR_DATA), false);
line.set_cell(2, CellData.from_char_data([0, '😁', 1, '😁'.ord_at(0)]))
line.set_cell(9, CellData.from_char_data([0, '😁', 1, '😁'.ord_at(0)]))
assert_eq(line.translate_to_string(), 'aa😁aaaaaa😁')
line.resize(5, CellData.from_char_data(CHAR_DATA))
assert_eq(line.translate_to_string(), 'aa😁aa')
line.resize(10, CellData.from_char_data(CHAR_DATA))
assert_eq(line.translate_to_string(), 'aa😁aaaaaaa')
class TestTrimLength:
extends "res://addons/gut/test.gd"
var line
func before_each():
line = BufferLineTest.new(3, CellData.from_char_data([Constants.DEFAULT_ATTR,
Constants.NULL_CELL_CHAR, Constants.NULL_CELL_WIDTH, Constants.NULL_CELL_CODE]))
func test_empty_line():
assert_eq(line.get_trimmed_length(), 0)
func test_ascii():
line.set_cell(0, CellData.from_char_data([1, "a", 1, "a".ord_at(0)]))
line.set_cell(2, CellData.from_char_data([1, "a", 1, "a".ord_at(0)]))
assert_eq(line.get_trimmed_length(), 3)
func test_unicode():
line.set_cell(0, CellData.from_char_data([1, "\u1f914", 1, "\u1f914".ord_at(0)]))
line.set_cell(2, CellData.from_char_data([1, "\u1f914", 1, "\u1f914".ord_at(0)]))
assert_eq(line.get_trimmed_length(), 3)
func test_one_cell():
line.set_cell(0, CellData.from_char_data([1, "a", 1, "a".ord_at(0)]))
assert_eq(line.get_trimmed_length(), 1)
class TestAddCharToCell:
extends "res://addons/gut/test.gd"
@ -293,7 +381,7 @@ class TestAddCharToCell:
assert_eq(cell.is_combined(), Content.IS_COMBINED_MASK)
class Testtranslate_to_string:
class TestTranslateToString:
extends "res://addons/gut/test.gd"

View file

@ -0,0 +1,201 @@
# Copyright (c) 2016 The xterm.js authors. All rights reserved.
# Ported to GDScript by the GodotXterm authors.
# License MIT
extends "res://addons/gut/test.gd"
const CIRCULAR_LIST_PATH = "res://addons/godot_xterm/circular_list.gd"
const CircularList = preload(CIRCULAR_LIST_PATH)
var list
func before_each():
list = CircularList.new(5)
func test_push():
list.push("1")
list.push("2")
list.push("3")
list.push("4")
list.push("5")
assert_eq(list.get_line(0), "1")
assert_eq(list.get_line(1), "2")
assert_eq(list.get_line(2), "3")
assert_eq(list.get_line(3), "4")
assert_eq(list.get_line(4), "5")
func test_splice_deletes_items():
list = CircularList.new(2)
list.push("1")
list.push("2")
list.splice(0, 1)
assert_eq(list.length, 1)
assert_eq(list.get_el(0), "2")
list.push("3")
list.splice(1, 1)
assert_eq(list.length, 1)
assert_eq(list.get_el(0), "2")
func test_splice_inserts_items():
list = CircularList.new(2)
list.push("1")
list.splice(0, 0, ["2"])
assert_eq(list.length, 2)
assert_eq(list.get_el(0), "2")
assert_eq(list.get_el(1), "1")
list.splice(1, 0, ["3"])
assert_eq(list.length, 2)
assert_eq(list.get_el(0), "3")
assert_eq(list.get_el(1), "1")
func test_splice_deletes_items_then_inserts_items():
list = CircularList.new(3)
list.push("1")
list.push("2")
list.splice(0, 1, ["3", "4"])
assert_eq(list.length, 3)
assert_eq(list.get_el(0), "3")
assert_eq(list.get_el(1), "4")
assert_eq(list.get_el(2), "2")
func test_splice_wraps_the_array_correctly_when_more_items_are_inserted_than_deleted():
list = CircularList.new(3)
list.push("1")
list.push("2")
list.splice(1, 0, ["3", "4"])
assert_eq(list.length, 3)
assert_eq(list.get_el(0), "3")
assert_eq(list.get_el(1), "4")
assert_eq(list.get_el(2), "2")
class TestShiftElements:
extends "res://addons/gut/test.gd"
var list
func before_each():
list = CircularList.new(5)
func test_does_not_mutate_the_list_when_count_is_0():
list.push(1)
list.push(2)
list.shift_elements(0, 0, 1)
assert_eq(list.length, 2)
assert_eq(list.get_el(0), 1)
assert_eq(list.get_el(1), 2)
func test_pushes_errors_for_invalid_args():
list = partial_double(CIRCULAR_LIST_PATH).new()
list.max_length = 5
list.push(1)
list.shift_elements(-1, 1, 1)
assert_called(list, "push_error", ["start argument out of range"])
list.shift_elements(1, 1, 1)
assert_called(list, "push_error", ["start argument out of range"])
list.shift_elements(0, 1, -1)
assert_called(list, "push_error", ["cannot shift elements in list beyond index 0"])
func test_trim_start_removes_items_from_the_beginning_of_the_list():
list.push("1")
list.push("2")
list.push("3")
list.push("4")
list.push("5")
list.trim_start(1)
assert_eq(list.length, 4)
assert_eq(list.get_el(0), "2")
assert_eq(list.get_el(1), "3")
assert_eq(list.get_el(2), "4")
assert_eq(list.get_el(3), "5")
list.trim_start(2)
assert_eq(list.length, 2)
assert_eq(list.get_el(0), "4")
assert_eq(list.get_el(1), "5")
func test_trim_start_removes_all_items_if_the_requested_trim_amount_is_larger_than_the_lists_length():
list.push("1")
list.trim_start(2)
assert_eq(list.length, 0)
func test_shifts_an_element_forward():
list.push(1)
list.push(2)
list.shift_elements(0, 1, 1)
assert_eq(list.length, 2)
assert_eq(list.get_el(0), 1)
assert_eq(list.get_el(1), 1)
func test_shifts_elements_forward():
list.push(1)
list.push(2)
list.push(3)
list.push(4)
list.shift_elements(0, 2, 2)
assert_eq(list.length, 4)
assert_eq(list.get_el(0), 1)
assert_eq(list.get_el(1), 2)
assert_eq(list.get_el(2), 1)
assert_eq(list.get_el(3), 2)
func test_shifts_elements_forward_expanding_the_list_if_needed():
list.push(1)
list.push(2)
list.shift_elements(0, 2, 2)
assert_eq(list.length, 4)
assert_eq(list.get_el(0), 1)
assert_eq(list.get_el(1), 2)
assert_eq(list.get_el(2), 1)
assert_eq(list.get_el(3), 2)
func test_shifts_elements_forward_wrapping_the_list_if_needed():
list.push(1)
list.push(2)
list.push(3)
list.push(4)
list.push(5)
list.shift_elements(2, 2, 3)
assert_eq(list.length, 5)
assert_eq(list.get_el(0), 3)
assert_eq(list.get_el(1), 4)
assert_eq(list.get_el(2), 5)
assert_eq(list.get_el(3), 3)
assert_eq(list.get_el(4), 4)
func test_shifts_an_element_backwards():
list.push(1)
list.push(2)
list.shift_elements(1, 1, -1)
assert_eq(list.length, 2)
assert_eq(list.get_el(0), 2)
assert_eq(list.get_el(1), 2)
func test_shiftS_elements_backwards():
list.push(1)
list.push(2)
list.push(3)
list.push(4)
list.shift_elements(2, 2, -2)
assert_eq(list.length, 4)
assert_eq(list.get_el(0), 3)
assert_eq(list.get_el(1), 4)
assert_eq(list.get_el(2), 3)
assert_eq(list.get_el(3), 4)

View file

@ -9,6 +9,10 @@ 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")
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
const OptionsService = preload("res://addons/godot_xterm/services/options_service.gd")
const CursorStyle = Constants.CursorStyle
var options_service
var buffer_service
@ -50,11 +54,75 @@ func before_each():
core_service = CoreService.new()
input_handler = InputHandler.new(buffer_service, core_service, charset_service, options_service)
# Skipping lots of tests here...
func test_save_and_restore_cursor():
buffer_service.buffer.x = 1
buffer_service.buffer.y = 2
buffer_service.buffer.ybase = 0
input_handler._cur_attr_data.fg = 3
# Save cursor position
input_handler.save_cursor()
assert_eq(buffer_service.buffer.x, 1)
assert_eq(buffer_service.buffer.y, 2)
assert_eq(input_handler._cur_attr_data.fg, 3)
# Change the cursor position
buffer_service.buffer.x = 10
buffer_service.buffer.y = 20
input_handler._cur_attr_data.fg = 30
# Restore cursor position
input_handler.restore_cursor()
assert_eq(buffer_service.buffer.x, 1)
assert_eq(buffer_service.buffer.y, 2)
assert_eq(input_handler._cur_attr_data.fg, 3)
func test_set_cursor_style():
input_handler.set_cursor_style(Params.from_array([0]))
assert_eq(options_service.options.cursor_style, CursorStyle.BLOCK)
assert_eq(options_service.options.cursor_blink, true)
options_service.options = OptionsService.TerminalOptions.new()
input_handler.set_cursor_style(Params.from_array([1]))
assert_eq(options_service.options.cursor_style, CursorStyle.BLOCK)
assert_eq(options_service.options.cursor_blink, true)
options_service.options = OptionsService.TerminalOptions.new()
input_handler.set_cursor_style(Params.from_array([2]))
assert_eq(options_service.options.cursor_style, CursorStyle.BLOCK)
assert_eq(options_service.options.cursor_blink, false)
options_service.options = OptionsService.TerminalOptions.new()
input_handler.set_cursor_style(Params.from_array([3]))
assert_eq(options_service.options.cursor_style, CursorStyle.UNDERLINE)
assert_eq(options_service.options.cursor_blink, true)
options_service.options = OptionsService.TerminalOptions.new()
input_handler.set_cursor_style(Params.from_array([4]))
assert_eq(options_service.options.cursor_style, CursorStyle.UNDERLINE)
assert_eq(options_service.options.cursor_blink, false)
options_service.options = OptionsService.TerminalOptions.new()
input_handler.set_cursor_style(Params.from_array([5]))
assert_eq(options_service.options.cursor_style, CursorStyle.BAR)
assert_eq(options_service.options.cursor_blink, true)
options_service.options = OptionsService.TerminalOptions.new()
input_handler.set_cursor_style(Params.from_array([6]))
assert_eq(options_service.options.cursor_style, CursorStyle.BAR)
assert_eq(options_service.options.cursor_blink, false)
func test_set_mode_toggles_bracketed_paste_mode():
# Set bracketed paste mode
input_handler.set_mode_private(Params.from_array([2004]))
assert_true(core_service.dec_private_modes.bracketed_paste_mode)
# Reset bracketed paste mode
input_handler.reset_mode_private(Params.from_array([2004]))
assert_false(core_service.dec_private_modes.bracketed_paste_mode)
func test_erase_in_line():
buffer_service = TestUtils.MockBufferService.new(10, 3, options_service)
buffer_service = TestUtils.MockBufferService.new(80, 30, options_service)
input_handler = InputHandler.new(buffer_service, core_service, charset_service, options_service)
# fill 6 lines to test 3 different states
@ -65,27 +133,27 @@ func test_erase_in_line():
# params[0] - right erase
buffer_service.buffer.y = 0
buffer_service.buffer.x = 7
buffer_service.buffer.x = 70
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) + " ")
assert_eq(buffer_service.buffer.lines.get_line(0).translate_to_string(false),
repeat("a", 70) + " ")
# # 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))
# 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_line(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_line(2).translate_to_string(false),
repeat(" ", buffer_service.cols))
func skip_test_erase_in_display():
func 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)
@ -169,36 +237,38 @@ func skip_test_erase_in_display():
"",
"",
]))
#
# # 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;
# reset and add a wrapped line
buffer_service.buffer.y = 0;
buffer_service.buffer.x = 0;
input_handler.parse(repeat("a", buffer_service.cols)) # line 0
input_handler.parse(repeat("a", buffer_service.cols + 9)) # line 1 and 2
for i in range(3, buffer_service.rows):
input_handler.parse(repeat("a", buffer_service.cols))
# params[1] left and above with wrap
# confirm precondition that line 2 is wrapped
assert_true(buffer_service.buffer.lines.get_line(2).is_wrapped)
buffer_service.buffer.y = 2
buffer_service.buffer.x = 40
input_handler.erase_in_display(Params.from_array([1]))
assert_false(buffer_service.buffer.lines.get_line(2).is_wrapped)
# reset and add a wrapped line
buffer_service.buffer.y = 0
buffer_service.buffer.x = 0
input_handler.parse(repeat("a", buffer_service.cols)) # line 0
input_handler.parse(repeat("a", buffer_service.cols + 9)) # line 1 and 2
for i in range(3, buffer_service.rows):
input_handler.parse(repeat("a", buffer_service.cols))
# params[1] left and above with wrap
# confirm precondition that line 2 is wrapped
assert_true(buffer_service.buffer.lines.get_line(2).is_wrapped)
buffer_service.buffer.y = 1
buffer_service.buffer.x = 90 # Cursor is beyond last column
input_handler.erase_in_display(Params.from_array([1]));
assert_false(buffer_service.buffer.lines.get_line(2).is_wrapped)
func test_print_does_not_cause_an_infinite_loop():

View file

@ -3,22 +3,27 @@
# 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)
@ -30,6 +35,7 @@ class TestParams:
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)
@ -43,6 +49,7 @@ class TestParams:
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)
@ -56,6 +63,7 @@ class TestParams:
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)
@ -94,11 +102,23 @@ class TestParams:
assert_eq(Params.from_array(data).to_array(), [12345, [-1]])
func test_has_sub_params_get_sub_params():
params = Params.from_array([38, [2, 50, 100, 150], 5, [], 6])
assert_eq(params.has_sub_params(0), true)
assert_eq(params.get_sub_params(0), [2, 50, 100, 150])
assert_eq(params.has_sub_params(1), false)
assert_eq(params.get_sub_params(1), null)
assert_eq(params.has_sub_params(2), false)
assert_eq(params.get_sub_params(2), null)
class TestParse:
extends 'res://addons/gut/test.gd'
var params
func parse(params, s):
params.reset()
params.add_param(0)
@ -125,17 +145,21 @@ class TestParse:
# 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])
@ -146,6 +170,7 @@ class TestParse:
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')
@ -163,6 +188,7 @@ class TestParse:
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')
@ -174,14 +200,17 @@ class TestParse:
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(), [
@ -190,13 +219,4 @@ class TestParse:
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]])