mirror of
https://github.com/lihop/godot-xterm.git
synced 2024-11-22 09:40:25 +01:00
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:
parent
0769592a1b
commit
0d4e10f5ab
30 changed files with 2640 additions and 1157 deletions
BIN
.test.txt.swp
BIN
.test.txt.swp
Binary file not shown.
|
@ -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)
|
|
|
@ -15,6 +15,12 @@ var bg = 0
|
||||||
var extended = ExtendedAttrs.new()
|
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
|
# flags
|
||||||
func is_inverse() -> int:
|
func is_inverse() -> int:
|
||||||
return fg & FgFlags.INVERSE
|
return fg & FgFlags.INVERSE
|
||||||
|
@ -27,7 +33,7 @@ func is_blink() -> int:
|
||||||
func is_invisible() -> int:
|
func is_invisible() -> int:
|
||||||
return fg & FgFlags.INVISIBLE
|
return fg & FgFlags.INVISIBLE
|
||||||
func is_italic() -> int:
|
func is_italic() -> int:
|
||||||
return fg & BgFlags.ITALIC
|
return bg & BgFlags.ITALIC
|
||||||
func is_dim() -> int:
|
func is_dim() -> int:
|
||||||
return fg & BgFlags.DIM
|
return fg & BgFlags.DIM
|
||||||
|
|
||||||
|
@ -60,13 +66,30 @@ func get_fg_color() -> int:
|
||||||
Attributes.CM_RGB:
|
Attributes.CM_RGB:
|
||||||
return fg & Attributes.RGB_MASK
|
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:
|
func has_extended_attrs() -> int:
|
||||||
return bg & BgFlags.HAS_EXTENDED
|
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:
|
func get_underline_color() -> int:
|
||||||
if bg & BgFlags.HAS_EXTENDED and ~extended.underline_color:
|
if bg & BgFlags.HAS_EXTENDED and ~extended.underline_color:
|
||||||
match extended.underline_color & Attributes.CM_MASK:
|
match extended.underline_color & Attributes.CM_MASK:
|
||||||
|
@ -117,6 +140,9 @@ func get_underline_style():
|
||||||
|
|
||||||
|
|
||||||
class ExtendedAttrs:
|
class ExtendedAttrs:
|
||||||
|
extends Reference
|
||||||
|
# Extended attributes for a cell.
|
||||||
|
# Holds information about different underline styles and color.
|
||||||
|
|
||||||
|
|
||||||
var underline_style = UnderlineStyle.NONE
|
var underline_style = UnderlineStyle.NONE
|
||||||
|
@ -125,3 +151,18 @@ class ExtendedAttrs:
|
||||||
|
|
||||||
func _init():
|
func _init():
|
||||||
underline_style
|
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
|
||||||
|
|
|
@ -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 Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
|
||||||
const CircularList = preload("res://addons/godot_xterm/circular_list.gd")
|
const CircularList = preload("res://addons/godot_xterm/circular_list.gd")
|
||||||
const AttributeData = preload("res://addons/godot_xterm/buffer/attribute_data.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
|
const MAX_BUFFER_SIZE = 4294967295 # 2^32 - 1
|
||||||
|
|
||||||
|
@ -51,6 +51,286 @@ func _init(has_scrollback: bool, options_service, buffer_service):
|
||||||
setup_tab_stops()
|
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):
|
func get_null_cell(attr = null):
|
||||||
if attr:
|
if attr:
|
||||||
_null_cell.fg = attr.fg
|
_null_cell.fg = attr.fg
|
||||||
|
@ -111,21 +391,23 @@ func get_wrapped_range_for_line(y: int) -> Dictionary:
|
||||||
return {"first": first, "last": last}
|
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:
|
func setup_tab_stops(i = null) -> void:
|
||||||
if i == null:
|
if i != null:
|
||||||
return
|
if not tabs.get(i):
|
||||||
|
i = prev_stop(i)
|
||||||
if not tabs.get(i):
|
|
||||||
i = prev_stop(i)
|
|
||||||
else:
|
else:
|
||||||
tabs = {}
|
tabs = {}
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
while i < _cols:
|
while i < _cols:
|
||||||
tabs[i] = true
|
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:
|
func prev_stop(x: int) -> int:
|
||||||
if x == null:
|
if x == null:
|
||||||
x = self.x
|
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
|
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
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,10 @@ func get_cell(index: int):
|
||||||
|
|
||||||
|
|
||||||
func get_width(index: int) -> int:
|
func get_width(index: int) -> int:
|
||||||
return _data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT
|
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:
|
func has_content(index: int) -> int:
|
||||||
|
@ -221,13 +224,43 @@ func fill(fill_cell_data) -> void:
|
||||||
set_cell(i, fill_cell_data)
|
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:
|
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:
|
if _data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK:
|
||||||
return i + (_data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT)
|
return i + (_data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT)
|
||||||
return 0
|
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:
|
func translate_to_string(trim_right: bool = false, start_col: int = 0, end_col: int = -1) -> String:
|
||||||
if end_col == -1:
|
if end_col == -1:
|
||||||
end_col = length
|
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
|
result += Constants.WHITESPACE_CELL_CHAR
|
||||||
start_col += max(content >> Content.WIDTH_SHIFT, 1) # always advance by 1
|
start_col += max(content >> Content.WIDTH_SHIFT, 1) # always advance by 1
|
||||||
return result
|
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
|
||||||
|
|
198
addons/godot_xterm/buffer/buffer_reflow.gd
Normal file
198
addons/godot_xterm/buffer/buffer_reflow.gd
Normal 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
|
|
@ -21,8 +21,7 @@ func _init(options_service, buffer_service):
|
||||||
alt = Buffer.new(false, options_service, buffer_service)
|
alt = Buffer.new(false, options_service, buffer_service)
|
||||||
active = normal
|
active = normal
|
||||||
|
|
||||||
# TODO
|
setup_tab_stops()
|
||||||
#setup_tab_stops()
|
|
||||||
|
|
||||||
|
|
||||||
# Sets the normal Bufer of the BufferSet as its currently active Buffer.
|
# 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
|
alt.y = normal.y
|
||||||
active = alt
|
active = alt
|
||||||
emit_signal("buffer_activated", alt, normal)
|
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)
|
||||||
|
|
|
@ -91,3 +91,16 @@ enum UnderlineStyle {
|
||||||
DOTTED
|
DOTTED
|
||||||
DASHED
|
DASHED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CursorStyle {
|
||||||
|
BLOCK
|
||||||
|
UNDERLINE
|
||||||
|
BAR
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BellStyle {
|
||||||
|
NONE
|
||||||
|
VISUAL
|
||||||
|
SOUND
|
||||||
|
BOTH
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@ extends Reference
|
||||||
# Represents a circular list; a list with a maximum size that wraps around when push is called,
|
# 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.
|
# overriding values at the start of the list.
|
||||||
|
|
||||||
signal deleted
|
|
||||||
|
signal deleted(index, amount)
|
||||||
signal inserted
|
signal inserted
|
||||||
signal trimmed
|
signal trimmed
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ var _array
|
||||||
var _start_index: int
|
var _start_index: int
|
||||||
var length: int = 0 setget _set_length,_get_length
|
var length: int = 0 setget _set_length,_get_length
|
||||||
var max_length: int setget _set_max_length,_get_max_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):
|
func _set_length(new_length: int):
|
||||||
|
@ -45,7 +47,12 @@ func _get_max_length():
|
||||||
return 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
|
self.max_length = max_length
|
||||||
_array = []
|
_array = []
|
||||||
_array.resize(max_length)
|
_array.resize(max_length)
|
||||||
|
@ -56,10 +63,23 @@ func get_el(index: int):
|
||||||
return _array[_get_cyclic_index(index)]
|
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:
|
func set_el(index: int, value) -> void:
|
||||||
_array[_get_cyclic_index(index)] = value
|
_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:
|
func push(value) -> void:
|
||||||
_array[_get_cyclic_index(length)] = value
|
_array[_get_cyclic_index(length)] = value
|
||||||
if length == max_length:
|
if length == max_length:
|
||||||
|
@ -70,6 +90,96 @@ func push(value) -> void:
|
||||||
length += 1
|
length += 1
|
||||||
|
|
||||||
|
|
||||||
func _get_cyclic_index(index: int) -> int:
|
# Advance ringbuffer index and return current element for recycling.
|
||||||
return _start_index + index % max_length
|
# 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)
|
||||||
|
|
|
@ -35,12 +35,12 @@ static func _generate_default_ansi_colors() -> PoolColorArray:
|
||||||
var r = v[(i / 36) % 6 | 0]
|
var r = v[(i / 36) % 6 | 0]
|
||||||
var g = v[(i / 6) % 6 | 0]
|
var g = v[(i / 6) % 6 | 0]
|
||||||
var b = v[i % 6]
|
var b = v[i % 6]
|
||||||
colors.append(Color(r, g, b))
|
colors.append(Color("%02x%02x%02x" % [r, g, b]))
|
||||||
|
|
||||||
# Generate greys (232-255)
|
# Generate greys (232-255)
|
||||||
for i in range(0, 24):
|
for i in range(0, 24):
|
||||||
var c = 8 + i * 10
|
var c = 8 + i * 10
|
||||||
colors.append(Color(c, c, c))
|
colors.append(Color("%02x%02x%02x" % [c, c, c]))
|
||||||
|
|
||||||
return colors
|
return colors
|
||||||
|
|
||||||
|
|
|
@ -2,23 +2,255 @@
|
||||||
# Ported to GDScript by the GodotXterm authors.
|
# Ported to GDScript by the GodotXterm authors.
|
||||||
# License MIT
|
# License MIT
|
||||||
extends Reference
|
extends Reference
|
||||||
|
|
||||||
|
|
||||||
# The character sets supported by the terminal. These enable several languages
|
# 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
|
# 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.
|
# for a discussion on character sets. Only VT100 character sets are supported.
|
||||||
|
|
||||||
const CHARSETS = {
|
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
|
# British character set
|
||||||
# ESC (A
|
# ESC (A
|
||||||
# Reference: http://vt100.net/docs/vt220-rm/table2-5.html
|
# Reference: http:#vt100.net/docs/vt220-rm/table2-5.html
|
||||||
'A': {
|
"A": {
|
||||||
'#': '£'
|
"#": "£"
|
||||||
},
|
},
|
||||||
|
|
||||||
# United States character set
|
# United States character set
|
||||||
# ESC (B
|
# 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.
|
# The default character set, US.
|
||||||
const DEFAULT_CHARSET = CHARSETS['B']
|
const DEFAULT_CHARSET = CHARSETS["B"]
|
||||||
|
|
|
@ -3,5 +3,4 @@
|
||||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.ttf" type="DynamicFontData" id=1]
|
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.ttf" type="DynamicFontData" id=1]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
size = 20
|
|
||||||
font_data = ExtResource( 1 )
|
font_data = ExtResource( 1 )
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
# Ported to GDScript by the GodotXterm authors.
|
# Ported to GDScript by the GodotXterm authors.
|
||||||
# License MIT
|
# License MIT
|
||||||
extends Reference
|
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 Constants = preload("res://addons/godot_xterm/parser/constants.gd")
|
||||||
const BufferConstants = preload("res://addons/godot_xterm/buffer/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 FgFlags = BufferConstants.FgFlags
|
||||||
const BgFlags = BufferConstants.BgFlags
|
const BgFlags = BufferConstants.BgFlags
|
||||||
const UnderlineStyle = BufferConstants.UnderlineStyle
|
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 GLEVEL = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2}
|
||||||
const MAX_PARSEBUFFER_LENGTH = 131072
|
const MAX_PARSEBUFFER_LENGTH = 131072
|
||||||
|
@ -59,7 +67,7 @@ func _get_buffer():
|
||||||
|
|
||||||
|
|
||||||
func _init(buffer_service, core_service, charset_service, options_service,
|
func _init(buffer_service, core_service, charset_service, options_service,
|
||||||
parser = EscapeSequenceParser.new()):
|
parser = EscapeSequenceParser.new()):
|
||||||
_buffer_service = buffer_service
|
_buffer_service = buffer_service
|
||||||
_core_service = core_service
|
_core_service = core_service
|
||||||
_charset_service = charset_service
|
_charset_service = charset_service
|
||||||
|
@ -69,72 +77,100 @@ func _init(buffer_service, core_service, charset_service, options_service,
|
||||||
buffer = _buffer_service.buffer
|
buffer = _buffer_service.buffer
|
||||||
_buffer_service.connect("buffer_activated", self, "_set_buffer")
|
_buffer_service.connect("buffer_activated", self, "_set_buffer")
|
||||||
|
|
||||||
|
|
||||||
# Print handler
|
# Print handler
|
||||||
_parser.set_print_handler(self, "print")
|
_parser.set_print_handler(self, "print")
|
||||||
|
|
||||||
# Execute handlers
|
# CSI handler
|
||||||
_parser.set_execute_handler(C0.BEL, self, 'bell')
|
_parser.set_csi_handler({"final": "@"}, self, "insert_chars")
|
||||||
_parser.set_execute_handler(C0.LF, self, 'line_feed')
|
_parser.set_csi_handler({"intermediates": " ", "final": "@"}, self, "scroll_left")
|
||||||
_parser.set_execute_handler(C0.VT, self, 'line_feed')
|
_parser.set_csi_handler({"final": "A"}, self, "cursor_up")
|
||||||
_parser.set_execute_handler(C0.FF, self, 'line_feed')
|
_parser.set_csi_handler({"intermediates": " ", "final": "A"}, self, "scroll_right")
|
||||||
_parser.set_execute_handler(C0.CR, self, 'carriage_return')
|
_parser.set_csi_handler({"final": "B"}, self, "cursor_down")
|
||||||
_parser.set_execute_handler(C0.BS, self, 'backspace')
|
_parser.set_csi_handler({"final": "C"}, self, "cursor_forward")
|
||||||
_parser.set_execute_handler(C0.HT, self, 'insert_tab');
|
_parser.set_csi_handler({"final": "D"}, self, "cursor_backward")
|
||||||
_parser.set_execute_handler(C0.SO, self, 'shift_out')
|
_parser.set_csi_handler({"final": "E"}, self, "cursor_nextLine")
|
||||||
_parser.set_execute_handler(C0.SI, self, 'shift_in')
|
_parser.set_csi_handler({"final": "F"}, self, "cursor_precedingLine")
|
||||||
_parser.set_execute_handler(C1.IND, self, 'index')
|
_parser.set_csi_handler({"final": "G"}, self, "cursor_char_absolute")
|
||||||
_parser.set_execute_handler(C1.NEL, self, 'next_line')
|
_parser.set_csi_handler({"final": "H"}, self, "cursor_position")
|
||||||
_parser.set_execute_handler(C1.HTS, self, 'tab_set')
|
_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
|
# execute handler
|
||||||
_parser.set_csi_handler({'final': '@'}, self, 'insert_chars')
|
_parser.set_execute_handler(C0.BEL, self, "bell")
|
||||||
_parser.set_csi_handler({'intermediates': ' ', 'final': '@'}, self, 'scroll_left')
|
_parser.set_execute_handler(C0.LF, self, "line_feed")
|
||||||
_parser.set_csi_handler({'final': 'A'}, self, 'cursor_up')
|
_parser.set_execute_handler(C0.VT, self, "line_feed")
|
||||||
_parser.set_csi_handler({'intermediates': ' ', 'final': 'A'}, self, 'scroll_right')
|
_parser.set_execute_handler(C0.FF, self, "line_feed")
|
||||||
_parser.set_csi_handler({'final': 'B'}, self, 'cursor_down')
|
_parser.set_execute_handler(C0.CR, self, "carriage_return")
|
||||||
_parser.set_csi_handler({'final': 'C'}, self, 'cursor_forward')
|
_parser.set_execute_handler(C0.BS, self, "backspace")
|
||||||
_parser.set_csi_handler({'final': 'D'}, self, 'cursor_backward')
|
_parser.set_execute_handler(C0.HT, self, "tab");
|
||||||
_parser.set_csi_handler({'final': 'E'}, self, 'cursor_nextLine')
|
_parser.set_execute_handler(C0.SO, self, "shift_out")
|
||||||
_parser.set_csi_handler({'final': 'F'}, self, 'cursor_precedingLine')
|
_parser.set_execute_handler(C0.SI, self, "shift_in")
|
||||||
_parser.set_csi_handler({'final': 'G'}, self, 'cursor_char_absolute')
|
# FIXME: What to do with missing? Old code just added those to print
|
||||||
_parser.set_csi_handler({'final': 'H'}, self, 'cursor_position')
|
|
||||||
_parser.set_csi_handler({'final': 'I'}, self, 'cursor_forward_tab')
|
_parser.set_execute_handler(C1.IND, self, "index")
|
||||||
_parser.set_csi_handler({'final': 'J'}, self, 'erase_in_display')
|
_parser.set_execute_handler(C1.NEL, self, "next_line")
|
||||||
_parser.set_csi_handler({'prefix': '?', 'final': 'J'}, self, 'erase_in_display')
|
_parser.set_execute_handler(C1.HTS, self, "tab_set")
|
||||||
_parser.set_csi_handler({'final': 'K'}, self, 'erase_in_line')
|
|
||||||
_parser.set_csi_handler({'prefix': '?', 'final': 'K'}, self, 'erase_in_line')
|
# ESC handlers
|
||||||
_parser.set_csi_handler({'final': 'L'}, self, 'insert_lines')
|
_parser.set_esc_handler({"final": "7"}, self, "save_cursor")
|
||||||
_parser.set_csi_handler({'final': 'M'}, self, 'delete_lines')
|
_parser.set_esc_handler({"final": "8"}, self, "restore_cursor")
|
||||||
_parser.set_csi_handler({'final': 'P'}, self, 'delete_chars')
|
_parser.set_esc_handler({"final": "D"}, self, "index")
|
||||||
_parser.set_csi_handler({'final': 'S'}, self, 'scroll_up')
|
_parser.set_esc_handler({"final": "E"}, self, "next_line")
|
||||||
_parser.set_csi_handler({'final': 'T'}, self, 'scroll_down')
|
_parser.set_esc_handler({"final": "H"}, self, "tab_set")
|
||||||
_parser.set_csi_handler({'final': 'X'}, self, 'erase_chars')
|
_parser.set_esc_handler({"final": "M"}, self, "reverse_index")
|
||||||
_parser.set_csi_handler({'final': 'Z'}, self, 'cursor_backward_tab')
|
_parser.set_esc_handler({"final": "="}, self, "keypad_application_mode")
|
||||||
_parser.set_csi_handler({'final': '`'}, self, 'char_pos_absolute')
|
_parser.set_esc_handler({"final": ">"}, self, "keypad_numeric_mode")
|
||||||
_parser.set_csi_handler({'final': 'a'}, self, 'h_position_relative')
|
_parser.set_esc_handler({"final": "c"}, self, "full_reset")
|
||||||
_parser.set_csi_handler({'final': 'b'}, self, 'repeat_preceding_character')
|
_parser.set_esc_handler({"final": "n"}, self, "set_glevel", 2)
|
||||||
_parser.set_csi_handler({'final': 'c'}, self, 'send_device_attributes_primary')
|
_parser.set_esc_handler({"final": "o"}, self, "set_glevel", 3)
|
||||||
_parser.set_csi_handler({'prefix': '>', 'final': 'c'}, self, 'send_device_attributes_secondary')
|
_parser.set_esc_handler({"final": "|"}, self, "set_glevel", 3)
|
||||||
_parser.set_csi_handler({'final': 'd'}, self, 'line_pos_absolute')
|
_parser.set_esc_handler({"final": "}"}, self, "set_glevel", 2)
|
||||||
_parser.set_csi_handler({'final': 'e'}, self, 'v_position_relative')
|
_parser.set_esc_handler({"final": "~"}, self, "set_glevel", 1)
|
||||||
_parser.set_csi_handler({'final': 'f'}, self, 'h_v_position')
|
_parser.set_esc_handler({"intermediates": "%", "final": "@"}, self, "select_default_charset")
|
||||||
_parser.set_csi_handler({'final': 'g'}, self, 'tab_clear')
|
_parser.set_esc_handler({"intermediates": "%", "final": "G"}, self, "select_default_charset")
|
||||||
_parser.set_csi_handler({'final': 'h'}, self, 'set_mode')
|
for flag in CHARSETS.keys():
|
||||||
_parser.set_csi_handler({'prefix': '?', 'final': 'h'}, self, 'set_mode_private')
|
_parser.set_esc_handler({"intermediates": "(", "final": flag}, self, "select_charset", "(" + flag)
|
||||||
_parser.set_csi_handler({'final': 'l'}, self, 'reset_mode')
|
_parser.set_esc_handler({"intermediates": ")", "final": flag}, self, "select_charset", ")" + flag)
|
||||||
_parser.set_csi_handler({'prefix': '?', 'final': 'l'}, self, 'reset_mode_private')
|
_parser.set_esc_handler({"intermediates": "*", "final": flag}, self, "select_charset", "*" + flag)
|
||||||
_parser.set_csi_handler({'final': 'm'}, self, 'char_attributes')
|
_parser.set_esc_handler({"intermediates": "+", "final": flag}, self, "select_charset", "+" + flag)
|
||||||
_parser.set_csi_handler({'final': 'n'}, self, 'device_status')
|
_parser.set_esc_handler({"intermediates": "-", "final": flag}, self, "select_charset", "-" + flag)
|
||||||
_parser.set_csi_handler({'prefix': '?', 'final': 'n'}, self, 'device_status_private')
|
_parser.set_esc_handler({"intermediates": ".", "final": flag}, self, "select_charset", "." + flag)
|
||||||
_parser.set_csi_handler({'intermediates': '!', 'final': 'p'}, self, 'soft_reset')
|
_parser.set_esc_handler({"intermediates": "/", "final": flag}, self, "select_charset", "/" + flag) # TODO: supported?
|
||||||
_parser.set_csi_handler({'intermediates': ' ', 'final': 'q'}, self, 'set_cursor_style')
|
_parser.set_esc_handler({"intermediates": "#", "final": "8"}, self, "screen_alignment_pattern")
|
||||||
_parser.set_csi_handler({'final': 'r'}, self, 'set_scroll_region')
|
|
||||||
_parser.set_csi_handler({'final': 's'}, self, 'save_cursor')
|
|
||||||
_parser.set_csi_handler({'final': 't'}, self, 'window_options')
|
|
||||||
_parser.set_csi_handler({'final': 'u'}, self, 'restore_cursor')
|
|
||||||
_parser.set_csi_handler({'intermediates': '\'', 'final': '}'}, self, 'insert_columns')
|
|
||||||
_parser.set_csi_handler({'intermediates': '\'', 'final': '~'}, self, 'delete_columns')
|
|
||||||
|
|
||||||
|
|
||||||
#func parse(data) -> void:
|
#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
|
# charset is only defined for ASCII, therefore we only
|
||||||
# search for an replacement char if code < 127
|
# search for an replacement char if code < 127
|
||||||
if code < 127 and charset:
|
if code < 127 and charset:
|
||||||
var ch = charset[char(code)]
|
var ch = charset.get(char(code))
|
||||||
if ch:
|
if ch:
|
||||||
code = ch.ord_at(0)
|
code = ch.ord_at(0)
|
||||||
|
|
||||||
|
@ -334,7 +370,7 @@ func line_feed():
|
||||||
buffer.y += 1
|
buffer.y += 1
|
||||||
if buffer.y == buffer.scroll_bottom + 1:
|
if buffer.y == buffer.scroll_bottom + 1:
|
||||||
buffer.y -= 1
|
buffer.y -= 1
|
||||||
emit_signal("scroll_requested")
|
emit_signal("scroll_requested", _erase_attr_data())
|
||||||
elif buffer.y >= _buffer_service.rows:
|
elif buffer.y >= _buffer_service.rows:
|
||||||
buffer.y = _buffer_service.rows - 1
|
buffer.y = _buffer_service.rows - 1
|
||||||
# If the end of the line is hit, prevent this action from wrapping around to the next line.
|
# If 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
|
# 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():
|
func shift_out():
|
||||||
_charset_service.set_glevel(1)
|
_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():
|
func shift_in():
|
||||||
_charset_service.get_glevel(0)
|
_charset_service.set_glevel(0)
|
||||||
|
|
||||||
|
|
||||||
func _restrict_cursor(max_col: int = _buffer_service.cols - 1) -> void:
|
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)
|
_set_cursor(self._buffer.y + x, self._buffer.y + y)
|
||||||
|
|
||||||
|
|
||||||
func index():
|
# ESC D
|
||||||
print("TODO: index")
|
# 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():
|
func tab_set():
|
||||||
print("TODO: tab_set")
|
buffer.tabs[buffer.x] = true
|
||||||
|
|
||||||
func insert_chars(params):
|
|
||||||
print("TODO: insert_chars")
|
|
||||||
|
|
||||||
func scroll_left(params):
|
# CSI Ps @
|
||||||
print("TODO: scroll_left")
|
# 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:
|
func cursor_up(params) -> void:
|
||||||
|
@ -464,15 +632,39 @@ func cursor_up(params) -> void:
|
||||||
_move_cursor(0, -params.get_param(0, 1))
|
_move_cursor(0, -params.get_param(0, 1))
|
||||||
|
|
||||||
|
|
||||||
func scroll_right(params):
|
# CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48
|
||||||
print("TODO: scroll_right")
|
#
|
||||||
|
# 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):
|
func cursor_down(params):
|
||||||
# stop at scroll_bottom
|
# stop at scroll_bottom
|
||||||
var diff_to_bottom = self._buffer.scroll_bottom - self._buffer.y
|
var diff_to_bottom = self._buffer.scroll_bottom - self._buffer.y
|
||||||
if diff_to_bottom >= 0:
|
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:
|
else:
|
||||||
_move_cursor(0, params.get_param(0, 1))
|
_move_cursor(0, params.get_param(0, 1))
|
||||||
|
|
||||||
|
@ -502,18 +694,18 @@ func cursor_char_absolute(params):
|
||||||
func cursor_position(params):
|
func cursor_position(params):
|
||||||
_set_cursor(
|
_set_cursor(
|
||||||
# col
|
# 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
|
# row
|
||||||
(params.get_param(0, 1)) - 1
|
(params.get_param(0, 1)) - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func char_pos_absolute(params) -> void:
|
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):
|
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):
|
func line_pos_absolute(params):
|
||||||
|
@ -521,12 +713,13 @@ func line_pos_absolute(params):
|
||||||
|
|
||||||
|
|
||||||
func v_position_relative(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):
|
func h_v_position(params):
|
||||||
cursor_position(params)
|
cursor_position(params)
|
||||||
|
|
||||||
|
|
||||||
# CSI Ps g Tab Clear (TBC).
|
# CSI Ps g Tab Clear (TBC).
|
||||||
# Ps = 0 -> Clear Current Column (default).
|
# Ps = 0 -> Clear Current Column (default).
|
||||||
# Ps = 3 -> Clear All.
|
# 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)."
|
# @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.
|
# Clearing tabstops off the active row (Ps = 2, VT100) is currently not supported.
|
||||||
func tab_clear(params) -> void:
|
func tab_clear(params) -> void:
|
||||||
match params[0]:
|
match params.get_param(0):
|
||||||
3:
|
3:
|
||||||
self._buffer.tabs = {}
|
self._buffer.tabs = {}
|
||||||
0, _:
|
0, _:
|
||||||
|
@ -551,7 +744,7 @@ func tab_clear(params) -> void:
|
||||||
func cursor_forward_tab(params) -> void:
|
func cursor_forward_tab(params) -> void:
|
||||||
if self._buffer.x >= self._buffer.cols:
|
if self._buffer.x >= self._buffer.cols:
|
||||||
return
|
return
|
||||||
var param = params[0] if params[0] else 1
|
var param = params.get_param(0, 1)
|
||||||
while param:
|
while param:
|
||||||
self._buffer.x = self._buffer.next_stop()
|
self._buffer.x = self._buffer.next_stop()
|
||||||
param -= 1
|
param -= 1
|
||||||
|
@ -560,7 +753,7 @@ func cursor_forward_tab(params) -> void:
|
||||||
func cursor_backward_tab(params) -> void:
|
func cursor_backward_tab(params) -> void:
|
||||||
if self._buffer.x >= _buffer_service.cols:
|
if self._buffer.x >= _buffer_service.cols:
|
||||||
return
|
return
|
||||||
var param = params[0] if params[0] else 1
|
var param = params.get_param(0, 1)
|
||||||
while param:
|
while param:
|
||||||
self._buffer.x = self._buffer.buffer.prev_stop()
|
self._buffer.x = self._buffer.buffer.prev_stop()
|
||||||
param -= 1
|
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.
|
# 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.
|
# 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:
|
func _reset_buffer_line(y: int) -> void:
|
||||||
var line = self._buffer.lines.get_el(self._buffer.ybase + y)
|
var line = buffer.lines.get_line(buffer.ybase + y)
|
||||||
line.fill(self._buffer.get_null_cell(_erase_attr_data()))
|
line.fill(buffer.get_null_cell(_erase_attr_data()))
|
||||||
line.is_wrapped = false
|
line.is_wrapped = false
|
||||||
|
|
||||||
|
|
||||||
|
@ -594,33 +787,32 @@ func erase_in_display(params) -> void:
|
||||||
var j
|
var j
|
||||||
match params.get_param(0):
|
match params.get_param(0):
|
||||||
0:
|
0:
|
||||||
j = self._buffer.y
|
j = buffer.y
|
||||||
# _dirty_row_service.mark_dirty(j)
|
# _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
|
j += 1
|
||||||
while j < _buffer_service.rows:
|
while j < _buffer_service.rows:
|
||||||
_reset_buffer_line(j)
|
_reset_buffer_line(j)
|
||||||
j += 1
|
j += 1
|
||||||
# _dirty_row_service.mark_dirty(j)
|
# _dirty_row_service.mark_dirty(j)
|
||||||
1:
|
1:
|
||||||
j = self._buffer.y
|
j = buffer.y
|
||||||
# _dirty_row_service.mark_dirty(j)
|
# _dirty_row_service.mark_dirty(j)
|
||||||
# Deleted front part of line and everything before. This line will no longer be wrapped.
|
# 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)
|
_erase_in_buffer_line(j, 0, buffer.x + 1, true)
|
||||||
if self._buffer.x + 1 >= _buffer_service.cols:
|
if buffer.x + 1 >= _buffer_service.cols:
|
||||||
# Deleted entire previous line. This next line can no longer be wrapped.
|
# 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
|
||||||
j -= 1
|
while j > 0:
|
||||||
while j >= 0:
|
|
||||||
_reset_buffer_line(j)
|
|
||||||
j -= 1
|
j -= 1
|
||||||
|
_reset_buffer_line(j)
|
||||||
# _dirty_row_service.mark_dirty(0)
|
# _dirty_row_service.mark_dirty(0)
|
||||||
2:
|
2:
|
||||||
j = _buffer_service.rows
|
j = _buffer_service.rows
|
||||||
# _dirty_row_service.mark_dirty(j - 1)
|
# _dirty_row_service.mark_dirty(j - 1)
|
||||||
while j:
|
while j > 0:
|
||||||
_reset_buffer_line(j)
|
|
||||||
j -= 1
|
j -= 1
|
||||||
|
_reset_buffer_line(j)
|
||||||
# _dirty_row_sevice.mark_dirty(0)
|
# _dirty_row_sevice.mark_dirty(0)
|
||||||
3:
|
3:
|
||||||
# Clear scrollback (everything not in viewport)
|
# 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)
|
_erase_in_buffer_line(buffer.y, 0, _buffer_service.cols)
|
||||||
|
|
||||||
|
|
||||||
func insert_lines(params):
|
# CSI Ps L
|
||||||
print("TODO: insert_lines")
|
# Insert Ps Line(s) (default = 1) (IL).
|
||||||
func delete_lines(params):
|
#
|
||||||
print("TODO: delete_lines")
|
# @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:
|
func delete_chars(params) -> void:
|
||||||
_restrict_cursor()
|
_restrict_cursor()
|
||||||
var line = buffer.lines.get_el(buffer.ybase + buffer.y)
|
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)
|
#_dirty_row_service.markDirty(buffer.y)
|
||||||
|
|
||||||
|
|
||||||
func scroll_up(params):
|
# CSI Ps S Scroll up Ps lines (default = 1) (SU).
|
||||||
print("TODO: scroll_up")
|
#
|
||||||
func scroll_down(params):
|
# @vt: #Y CSI SU "Scroll Up" "CSI Ps S" "Scroll `Ps` lines up (default=1)."
|
||||||
print("TODO: scroll_down")
|
#
|
||||||
|
#
|
||||||
|
# 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:
|
func erase_chars(params) -> void:
|
||||||
|
@ -686,15 +958,19 @@ func repeat_preceding_character(params) -> void:
|
||||||
|
|
||||||
|
|
||||||
func send_device_attributes_primary(params):
|
func send_device_attributes_primary(params):
|
||||||
print("TODO: send dev attr primary")
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
func send_device_attributes_secondary(params):
|
func send_device_attributes_secondary(params):
|
||||||
print("TODO: send dev attr second")
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func set_mode(params):
|
func set_mode(params):
|
||||||
print("TODO: set mode")
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
func reset_mode(params) -> void:
|
func reset_mode(params) -> void:
|
||||||
for param in params.params:
|
for param in params.params:
|
||||||
|
@ -708,14 +984,15 @@ func reset_mode(params) -> void:
|
||||||
|
|
||||||
func char_attributes(params):
|
func char_attributes(params):
|
||||||
# Optimize a single SGR0
|
# Optimize a single SGR0
|
||||||
if params.size() == 1 and params[0] == 0:
|
if params.length == 1 and params.get_param(0) == 0:
|
||||||
_cur_attr_data.fg = DEFAULT_ATTRIBUTE_DATA.fg
|
_cur_attr_data.fg = AttributeData.new().fg
|
||||||
_cur_attr_data.bg = DEFAULT_ATTRIBUTE_DATA.bg
|
_cur_attr_data.bg = AttributeData.new().bg
|
||||||
return
|
return
|
||||||
|
|
||||||
var attr = _cur_attr_data
|
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:
|
if p >= 30 and p <= 37:
|
||||||
# fg color 8
|
# fg color 8
|
||||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK)
|
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK)
|
||||||
|
@ -745,17 +1022,140 @@ func char_attributes(params):
|
||||||
elif p == 4:
|
elif p == 4:
|
||||||
# underlined text
|
# underlined text
|
||||||
attr.fg |= FgFlags.UNDERLINE
|
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):
|
func device_status(params):
|
||||||
print("TODO: dev stat")
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
func device_status_private(params):
|
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):
|
func soft_reset(params):
|
||||||
print("TODO: soft reset")
|
_core_service.is_cursor_hidden = false
|
||||||
func set_cursor_style(params):
|
emit_signal("scrollbar_sync_requested")
|
||||||
print("TODO: set cur styl")
|
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:
|
func set_scroll_region(params) -> void:
|
||||||
|
@ -794,11 +1194,44 @@ func window_options(params):
|
||||||
pass
|
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):
|
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):
|
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:
|
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
|
_core_service.dec_private_modes.bracketed_paste_mode = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func reset_mode_private(params):
|
func reset_mode_private(params):
|
||||||
for param in params.to_array():
|
for param in params.to_array():
|
||||||
match param:
|
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.
|
# Helper to extract and apply color params/subparams.
|
||||||
# Returns advance for params index.
|
# Returns advance for params index.
|
||||||
func _extract_color(params: Array, pos: int, attr) -> int:
|
func _extract_color(params, pos: int, attr) -> int:
|
||||||
# normalize params
|
# normalize params
|
||||||
# meaning: [target, CM, ign, val, val, val]
|
# meaning: [target, CM, ign, val, val, val]
|
||||||
# RGB : [ 38/34, 2, ign, r, g, b]
|
# 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
|
# return advance we took in params
|
||||||
var advance = -1
|
var advance = -1
|
||||||
|
|
||||||
while advance + pos < params.size() and advance + c_space < accu.size():
|
while advance + pos < params.length and advance + c_space < accu.size():
|
||||||
accu[advance + c_space] = params[pos + advance]
|
accu[advance + c_space] = params.get_param(pos + advance)
|
||||||
advance += 1
|
advance += 1
|
||||||
# TODO FIX and FINISH me
|
# TODO FIX and FINISH me
|
||||||
return advance
|
return advance
|
||||||
|
@ -983,17 +1415,16 @@ func restore_cursor(params = null) -> void:
|
||||||
#
|
#
|
||||||
func reverse_index() -> void:
|
func reverse_index() -> void:
|
||||||
_restrict_cursor()
|
_restrict_cursor()
|
||||||
# if self._buffer.y == self._buffer.scroll_top:
|
if buffer.y == buffer.scroll_top:
|
||||||
# # possibly move the code below to term.reverse_scroll()
|
# possibly move the code below to term.reverse_srcoll()
|
||||||
# # test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
|
# test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
|
||||||
# # blankLine(true) is xterm/linux behavior
|
# blank_line(true) is xterm/linux behavior
|
||||||
# var scroll_region_height = self._buffer.scroll_bottom - self._buffer.scroll_top
|
var scroll_region_height = buffer.scroll_bottom - buffer.scroll_top
|
||||||
# self._buffer.lines.shiftElements(buffer.ybase + buffer.y, scrollRegionHeight, 1);
|
buffer.lines.shift_elements(buffer.ybase + buffer.y, scroll_region_height, 1)
|
||||||
# buffer.lines.set(buffer.ybase + buffer.y, buffer.getBlankLine(this._eraseAttrData()));
|
buffer.lines.set_line(buffer.ybase + buffer.y, buffer.get_blank_line(_erase_attr_data()))
|
||||||
# this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
|
else:
|
||||||
# else
|
buffer.y -= 1
|
||||||
# self._buffer.y -= 1
|
_restrict_cursor() # quickfix to not run out of bounds
|
||||||
# _restrict_cursor() # quickfix to not run out of bounds
|
|
||||||
|
|
||||||
|
|
||||||
# ESC c
|
# ESC c
|
||||||
|
@ -1012,7 +1443,7 @@ func reset() -> void:
|
||||||
func _process_underline(style: int, attr) -> void:
|
func _process_underline(style: int, attr) -> void:
|
||||||
# treat extended attrs as immutable, thus always clone from old one
|
# treat extended attrs as immutable, thus always clone from old one
|
||||||
# this is needed since the buffer only holds references to it
|
# 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
|
# default to 1 == single underline
|
||||||
if not ~style or style > 5:
|
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 &= ~(Attributes.CM_MASK | 0xFFFFFF)
|
||||||
_erase_attr_data_internal.bg |= _cur_attr_data.bg & ~0xFC000000
|
_erase_attr_data_internal.bg |= _cur_attr_data.bg & ~0xFC000000
|
||||||
return _erase_attr_data_internal
|
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)
|
||||||
|
|
|
@ -145,8 +145,9 @@ func set_execute_handler_fallback(target: Object, method: String):
|
||||||
_execute_handler_fb = { 'target': target, 'method': method }
|
_execute_handler_fb = { 'target': target, 'method': method }
|
||||||
|
|
||||||
|
|
||||||
func set_esc_handler(id, target, method):
|
func set_esc_handler(id, target, method, arg = null):
|
||||||
_esc_handlers[identifier(id, [0x30, 0x7e])] = [{'target': target, 'method': method}]
|
_esc_handlers[identifier(id, [0x30, 0x7e])] = [{'target': target,
|
||||||
|
'method': method, 'arg': arg}]
|
||||||
|
|
||||||
|
|
||||||
func set_esc_handler_fallback(target: Object, method: String):
|
func set_esc_handler_fallback(target: Object, method: String):
|
||||||
|
@ -229,6 +230,7 @@ func parse(data: Array, length: int):
|
||||||
ParserAction.EXECUTE:
|
ParserAction.EXECUTE:
|
||||||
var handler = _execute_handlers.get(code)
|
var handler = _execute_handlers.get(code)
|
||||||
if handler:
|
if handler:
|
||||||
|
print("EXEC: ", handler['method'])
|
||||||
handler['target'].call(handler['method'])
|
handler['target'].call(handler['method'])
|
||||||
elif _execute_handler_fb:
|
elif _execute_handler_fb:
|
||||||
_execute_handler_fb['target'].call(_execute_handler_fb['method'], code)
|
_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), [])
|
var handlers = _csi_handlers.get((collect << 8 | code), [])
|
||||||
handlers.invert()
|
handlers.invert()
|
||||||
for handler in handlers:
|
for handler in handlers:
|
||||||
|
print("CSI: ", handler['method'])
|
||||||
# undefined or true means success and to stop bubbling
|
# undefined or true means success and to stop bubbling
|
||||||
if handler['target'].call(handler['method'], params):
|
if handler['target'].call(handler['method'], params):
|
||||||
continue
|
continue
|
||||||
|
@ -277,8 +280,13 @@ func parse(data: Array, length: int):
|
||||||
handlers.invert()
|
handlers.invert()
|
||||||
for handler in handlers:
|
for handler in handlers:
|
||||||
# undefined or true means success and to stop bubbling
|
# undefined or true means success and to stop bubbling
|
||||||
if handler['target'].call(handler['method']) != false:
|
print("ESC: ", handler['method'])
|
||||||
continue
|
if handler['arg']:
|
||||||
|
if handler['target'].call(handler['method'], handler['arg']) != false:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if handler['target'].call(handler['method']) != false:
|
||||||
|
continue
|
||||||
handlers.invert()
|
handlers.invert()
|
||||||
if handlers.empty():
|
if handlers.empty():
|
||||||
_esc_handler_fb['target'].call(_esc_handler_fb['method'], collect << 8 | code)
|
_esc_handler_fb['target'].call(_esc_handler_fb['method'], collect << 8 | code)
|
||||||
|
|
|
@ -75,6 +75,12 @@ func add_param(value: int):
|
||||||
params[length] = MAX_VALUE if value > MAX_VALUE else value
|
params[length] = MAX_VALUE if value > MAX_VALUE else value
|
||||||
length += 1
|
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):
|
func add_sub_param(value: int):
|
||||||
digit_is_sub = true
|
digit_is_sub = true
|
||||||
if !length:
|
if !length:
|
||||||
|
@ -88,6 +94,21 @@ func add_sub_param(value: int):
|
||||||
sub_params_length += 1
|
sub_params_length += 1
|
||||||
sub_params_idx[length - 1] += 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):
|
func add_digit(value: int):
|
||||||
var _length = sub_params_length if digit_is_sub else length
|
var _length = sub_params_length if digit_is_sub else length
|
||||||
if _reject_digits or (not _length) or (digit_is_sub and _reject_sub_digits):
|
if _reject_digits or (not _length) or (digit_is_sub and _reject_sub_digits):
|
||||||
|
@ -97,10 +118,6 @@ func add_digit(value: int):
|
||||||
store[_length - 1] = min(cur * 10 + value, MAX_VALUE) if ~cur else value
|
store[_length - 1] = min(cur * 10 + value, MAX_VALUE) if ~cur else value
|
||||||
|
|
||||||
|
|
||||||
func size():
|
|
||||||
return params.size()
|
|
||||||
|
|
||||||
|
|
||||||
func to_array():
|
func to_array():
|
||||||
var res = []
|
var res = []
|
||||||
for i in range(length):
|
for i in range(length):
|
||||||
|
|
|
@ -1,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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -4,7 +4,7 @@
|
||||||
extends Reference
|
extends Reference
|
||||||
|
|
||||||
signal buffer_activated(active_buffer, inactive_buffer)
|
signal buffer_activated(active_buffer, inactive_buffer)
|
||||||
signal resized
|
signal resized(cols, rows)
|
||||||
|
|
||||||
const BufferSet = preload("res://addons/godot_xterm/buffer/buffer_set.gd")
|
const BufferSet = preload("res://addons/godot_xterm/buffer/buffer_set.gd")
|
||||||
|
|
||||||
|
@ -29,11 +29,25 @@ func _get_buffer():
|
||||||
|
|
||||||
func _init(options_service):
|
func _init(options_service):
|
||||||
_options_service = options_service
|
_options_service = options_service
|
||||||
|
_options_service.connect("option_changed", self, "_option_changed")
|
||||||
cols = max(_options_service.options.cols, MINIMUM_COLS)
|
cols = max(_options_service.options.cols, MINIMUM_COLS)
|
||||||
rows = max(_options_service.options.rows, MINIMUM_ROWS)
|
rows = max(_options_service.options.rows, MINIMUM_ROWS)
|
||||||
buffers = BufferSet.new(_options_service, self)
|
buffers = BufferSet.new(_options_service, self)
|
||||||
buffers.connect("buffer_activated", self, "_buffer_activated")
|
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):
|
func _buffer_activated(active_buffer, inactive_buffer):
|
||||||
emit_signal("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)
|
||||||
|
|
|
@ -4,45 +4,72 @@
|
||||||
extends Reference
|
extends Reference
|
||||||
|
|
||||||
|
|
||||||
class TerminalOptions:
|
const Constants = preload("res://addons/godot_xterm/buffer/constants.gd")
|
||||||
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 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
|
signal option_changed
|
||||||
|
|
||||||
|
@ -51,3 +78,27 @@ var options
|
||||||
|
|
||||||
func _init(options):
|
func _init(options):
|
||||||
self.options = 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
|
||||||
|
|
|
@ -33,9 +33,9 @@ const Const = preload("res://addons/godot_xterm/Constants.gd")
|
||||||
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
|
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
|
||||||
const Parser = preload("res://addons/godot_xterm/parser/escape_sequence_parser.gd")
|
const Parser = preload("res://addons/godot_xterm/parser/escape_sequence_parser.gd")
|
||||||
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
|
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
|
||||||
const Renderer = preload("res://addons/godot_xterm/renderer/renderer.gd")
|
|
||||||
const ColorManager = preload("res://addons/godot_xterm/color_manager.gd")
|
const ColorManager = preload("res://addons/godot_xterm/color_manager.gd")
|
||||||
const CellData = preload("res://addons/godot_xterm/buffer/cell_data.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 SourceCodeProRegular = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres")
|
||||||
const SourceCodeProBold = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres")
|
const SourceCodeProBold = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres")
|
||||||
|
@ -52,15 +52,21 @@ const LEFT_BRACKET = 91
|
||||||
const ENTER = 10
|
const ENTER = 10
|
||||||
const BACKSPACE_ALT = 127
|
const BACKSPACE_ALT = 127
|
||||||
|
|
||||||
|
const BLINK_INTERVAL = 0.6 # 600ms. The time between blinks.
|
||||||
|
|
||||||
# TODO: Move me somewhere else.
|
# TODO: Move me somewhere else.
|
||||||
enum BellStyle {
|
enum BellStyle {
|
||||||
NONE
|
NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
signal output(data)
|
signal output(data)
|
||||||
|
signal scrolled(ydisp)
|
||||||
|
|
||||||
export var cols = 80
|
export var cols = 80
|
||||||
export var rows = 24
|
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_blink = false
|
||||||
export var cursor_style = 'block'
|
export var cursor_style = 'block'
|
||||||
export var cursor_width = 1
|
export var cursor_width = 1
|
||||||
|
@ -76,8 +82,6 @@ export var font_family: Dictionary = {
|
||||||
"bold_italic": SourceCodeProBoldItalic,
|
"bold_italic": SourceCodeProBoldItalic,
|
||||||
}
|
}
|
||||||
export var font_size: int = 15
|
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 line_height = 1.0
|
||||||
export var link_tooltip_hover_duration = 500 # Not relevant?
|
export var link_tooltip_hover_duration = 500 # Not relevant?
|
||||||
export var letter_spacing = 0
|
export var letter_spacing = 0
|
||||||
|
@ -93,7 +97,22 @@ export var allow_proposed_api = true
|
||||||
export var allow_transparency = false
|
export var allow_transparency = false
|
||||||
export var tab_stop_width = 8
|
export var tab_stop_width = 8
|
||||||
export var colors: Dictionary = {
|
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 right_click_selects_word = 'isMac' # TODO
|
||||||
export var renderer_type = 'canvas' # Relevant?
|
export var renderer_type = 'canvas' # Relevant?
|
||||||
|
@ -122,19 +141,13 @@ var _scaled_cell_height
|
||||||
var _scaled_char_top
|
var _scaled_char_top
|
||||||
var _scaled_char_left
|
var _scaled_char_left
|
||||||
var _work_cell = CellData.new()
|
var _work_cell = CellData.new()
|
||||||
|
var _blink_on = false
|
||||||
|
var _time_since_last_blink = 0
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
var options = OptionsService.TerminalOptions.new()
|
var options = OptionsService.TerminalOptions.new()
|
||||||
options.cols = cols
|
options.copy_from(self)
|
||||||
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_service = OptionsService.new(options)
|
options_service = OptionsService.new(options)
|
||||||
options_service.connect("option_changed", self, "_update_options")
|
|
||||||
|
|
||||||
_buffer_service = BufferService.new(options_service)
|
_buffer_service = BufferService.new(options_service)
|
||||||
_core_service = CoreService.new()
|
_core_service = CoreService.new()
|
||||||
|
@ -151,9 +164,10 @@ func _ready():
|
||||||
|
|
||||||
_color_manager = ColorManager.new()
|
_color_manager = ColorManager.new()
|
||||||
_color_manager.set_theme(colors)
|
_color_manager.set_theme(colors)
|
||||||
_render_service = Renderer.new(_color_manager.colors, self, _buffer_service, options_service)
|
|
||||||
|
|
||||||
connect("resized", self, "_update_dimensions")
|
if auto_resize:
|
||||||
|
connect("resized", self, "_update_dimensions")
|
||||||
|
|
||||||
_update_dimensions()
|
_update_dimensions()
|
||||||
|
|
||||||
|
|
||||||
|
@ -232,20 +246,145 @@ func _update_dimensions():
|
||||||
# order to draw in the center of a cell.
|
# order to draw in the center of a cell.
|
||||||
_scaled_char_left = floor(options_service.options.letter_spacing / 2)
|
_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():
|
func _draw():
|
||||||
# Draw the background and foreground
|
# Draw the background and foreground
|
||||||
|
if _buffer_service == null:
|
||||||
|
return
|
||||||
|
|
||||||
var buffer = _buffer_service.buffer
|
var buffer = _buffer_service.buffer
|
||||||
for y in range(buffer.ybase, rows):
|
var rows = _buffer_service.rows
|
||||||
var line = buffer.lines.get_el(y)
|
|
||||||
|
for y in range(0, rows):
|
||||||
|
var row = y + buffer.ydisp
|
||||||
|
var line = buffer.lines.get_line(row)
|
||||||
for x in line.length:
|
for x in line.length:
|
||||||
line.load_cell(x, _work_cell)
|
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,
|
draw_rect(Rect2(x * _scaled_cell_width, y * _scaled_cell_height,
|
||||||
(cols - x) * _scaled_cell_width, 1 * _scaled_cell_height), Color())
|
(cols - x) * _scaled_cell_width, 1 * _scaled_cell_height),
|
||||||
var color = _color_manager.colors.ansi[_work_cell.get_fg_color()] if _work_cell.get_fg_color() >= 0 else Color(1, 1, 1)
|
bg_color)
|
||||||
draw_char(options_service.options.font_family.regular,
|
|
||||||
|
# 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,
|
Vector2(x * _scaled_cell_width + _scaled_char_left,
|
||||||
y * _scaled_cell_height + _scaled_char_top + _scaled_char_height / 2),
|
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 the cursor
|
||||||
# Draw selection
|
# Draw selection
|
||||||
|
|
61
demo.cast
Normal file
61
demo.cast
Normal 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"]
|
|
@ -38,7 +38,6 @@ func _ready():
|
||||||
if err != OK:
|
if err != OK:
|
||||||
OS.alert("Couldn't connect to socat on %s:%d" % [host, port], "Connection Failed!")
|
OS.alert("Couldn't connect to socat on %s:%d" % [host, port], "Connection Failed!")
|
||||||
|
|
||||||
|
|
||||||
var status = stream_peer.get_status()
|
var status = stream_peer.get_status()
|
||||||
var connected = stream_peer.is_connected_to_host()
|
var connected = stream_peer.is_connected_to_host()
|
||||||
|
|
||||||
|
@ -52,6 +51,13 @@ func _ready():
|
||||||
$Terminal.connect('output', self, 'send_data')
|
$Terminal.connect('output', self, 'send_data')
|
||||||
connect("data_received", $Terminal, "write")
|
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):
|
func send_data(data: PoolByteArray):
|
||||||
if record:
|
if record:
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres" type="DynamicFont" id=6]
|
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres" type="DynamicFont" id=6]
|
||||||
|
|
||||||
[node name="Demo" type="Control"]
|
[node name="Demo" type="Control"]
|
||||||
|
show_behind_parent = true
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
script = ExtResource( 1 )
|
script = ExtResource( 1 )
|
||||||
|
@ -16,25 +17,39 @@ __meta__ = {
|
||||||
}
|
}
|
||||||
|
|
||||||
[node name="Terminal" type="Control" parent="."]
|
[node name="Terminal" type="Control" parent="."]
|
||||||
margin_left = 95.937
|
anchor_right = 1.0
|
||||||
margin_top = 44.6138
|
anchor_bottom = 1.0
|
||||||
margin_right = 695.937
|
|
||||||
margin_bottom = 444.614
|
|
||||||
rect_min_size = Vector2( 600, 400 )
|
rect_min_size = Vector2( 600, 400 )
|
||||||
script = ExtResource( 2 )
|
script = ExtResource( 2 )
|
||||||
__meta__ = {
|
__meta__ = {
|
||||||
"_edit_use_anchors_": false
|
"_edit_use_anchors_": false
|
||||||
}
|
}
|
||||||
|
auto_resize = true
|
||||||
font_family = {
|
font_family = {
|
||||||
"bold": ExtResource( 6 ),
|
"bold": ExtResource( 6 ),
|
||||||
"bold_italic": ExtResource( 5 ),
|
"bold_italic": ExtResource( 5 ),
|
||||||
"italic": ExtResource( 4 ),
|
"italic": ExtResource( 4 ),
|
||||||
"regular": ExtResource( 3 )
|
"regular": ExtResource( 3 )
|
||||||
}
|
}
|
||||||
font_size = 16
|
line_height = 1.15
|
||||||
colors = {
|
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 = {
|
window_options = {
|
||||||
|
"set_win_lines": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,9 +100,3 @@ func test_csi_position_cursor():
|
||||||
parse(parser, '\u001b[1;5H')
|
parse(parser, '\u001b[1;5H')
|
||||||
assert_eq(buffer.calls, [['csi', [1, 5]]])
|
assert_eq(buffer.calls, [['csi', [1, 5]]])
|
||||||
assert_eq(buffer.printed, '')
|
assert_eq(buffer.printed, '')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -124,5 +124,239 @@ class TestGetWrappedRangeForLine:
|
||||||
assert_eq(buffer.get_wrapped_range_for_line(buffer.lines.length - 2).last, INIT_ROWS - 1)
|
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)
|
||||||
|
|
|
@ -146,7 +146,6 @@ class TestCellData:
|
||||||
cell = CellData.new()
|
cell = CellData.new()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func test_char_data_cell_data_equality():
|
func test_char_data_cell_data_equality():
|
||||||
# ASCII
|
# ASCII
|
||||||
cell.set_from_char_data([123, 'a', 1, 'a'.ord_at(0)])
|
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)],
|
[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:
|
class TestAddCharToCell:
|
||||||
extends "res://addons/gut/test.gd"
|
extends "res://addons/gut/test.gd"
|
||||||
|
@ -293,7 +381,7 @@ class TestAddCharToCell:
|
||||||
assert_eq(cell.is_combined(), Content.IS_COMBINED_MASK)
|
assert_eq(cell.is_combined(), Content.IS_COMBINED_MASK)
|
||||||
|
|
||||||
|
|
||||||
class Testtranslate_to_string:
|
class TestTranslateToString:
|
||||||
extends "res://addons/gut/test.gd"
|
extends "res://addons/gut/test.gd"
|
||||||
|
|
||||||
|
|
||||||
|
|
201
test/unit/test_circular_list.gd
Normal file
201
test/unit/test_circular_list.gd
Normal 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)
|
|
@ -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 CharsetService = preload("res://addons/godot_xterm/services/charset_service.gd")
|
||||||
const Params = preload("res://addons/godot_xterm/parser/params.gd")
|
const Params = preload("res://addons/godot_xterm/parser/params.gd")
|
||||||
const CoreService = preload("res://addons/godot_xterm/services/core_service.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 options_service
|
||||||
var buffer_service
|
var buffer_service
|
||||||
|
@ -50,11 +54,75 @@ func before_each():
|
||||||
core_service = CoreService.new()
|
core_service = CoreService.new()
|
||||||
input_handler = InputHandler.new(buffer_service, core_service, charset_service, options_service)
|
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():
|
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)
|
input_handler = InputHandler.new(buffer_service, core_service, charset_service, options_service)
|
||||||
|
|
||||||
# fill 6 lines to test 3 different states
|
# fill 6 lines to test 3 different states
|
||||||
|
@ -65,27 +133,27 @@ func test_erase_in_line():
|
||||||
|
|
||||||
# params[0] - right erase
|
# params[0] - right erase
|
||||||
buffer_service.buffer.y = 0
|
buffer_service.buffer.y = 0
|
||||||
buffer_service.buffer.x = 7
|
buffer_service.buffer.x = 70
|
||||||
input_handler.erase_in_line(Params.from_array([0]))
|
input_handler.erase_in_line(Params.from_array([0]))
|
||||||
assert_eq(buffer_service.buffer.lines.get_el(0).translate_to_string(false),
|
assert_eq(buffer_service.buffer.lines.get_line(0).translate_to_string(false),
|
||||||
repeat("a", 7) + " ")
|
repeat("a", 70) + " ")
|
||||||
|
|
||||||
# # params[1] - left erase
|
# params[1] - left erase
|
||||||
# buffer_service.buffer.y = 1
|
buffer_service.buffer.y = 1
|
||||||
# buffer_service.buffer.x = 70
|
buffer_service.buffer.x = 70
|
||||||
# input_handler.erase_in_line(Params.from_array([1]))
|
input_handler.erase_in_line(Params.from_array([1]))
|
||||||
# assert_eq(buffer_service.buffer.lines.get_el(1).translate_to_string(false),
|
assert_eq(buffer_service.buffer.lines.get_line(1).translate_to_string(false),
|
||||||
# repeat(" ", 70) + " aaaaaaaaa")
|
repeat(" ", 70) + " aaaaaaaaa")
|
||||||
#
|
|
||||||
# # params[1] - left erase
|
# params[1] - left erase
|
||||||
# buffer_service.buffer.y = 2
|
buffer_service.buffer.y = 2
|
||||||
# buffer_service.buffer.x = 70
|
buffer_service.buffer.x = 70
|
||||||
# input_handler.erase_in_line(Params.from_array([2]))
|
input_handler.erase_in_line(Params.from_array([2]))
|
||||||
# assert_eq(buffer_service.buffer.lines.get_el(2).translate_to_string(false),
|
assert_eq(buffer_service.buffer.lines.get_line(2).translate_to_string(false),
|
||||||
# repeat(" ", buffer_service.cols))
|
repeat(" ", buffer_service.cols))
|
||||||
|
|
||||||
|
|
||||||
func skip_test_erase_in_display():
|
func test_erase_in_display():
|
||||||
buffer_service = TestUtils.MockBufferService.new(80, 7, options_service)
|
buffer_service = TestUtils.MockBufferService.new(80, 7, options_service)
|
||||||
input_handler = InputHandler.new(buffer_service, core_service, charset_service, 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
|
# reset and add a wrapped line
|
||||||
# buffer_service.buffer.y = 0;
|
buffer_service.buffer.y = 0;
|
||||||
# buffer_service.buffer.x = 0;
|
buffer_service.buffer.x = 0;
|
||||||
# input_handler.parse(Array(buffer_service.cols + 1).join('a')); # line 0
|
input_handler.parse(repeat("a", buffer_service.cols)) # line 0
|
||||||
# input_handler.parse(Array(buffer_service.cols + 10).join('a')); # line 1 and 2
|
input_handler.parse(repeat("a", buffer_service.cols + 9)) # line 1 and 2
|
||||||
# for (let i = 3; i < buffer_service.rows; ++i) input_handler.parse(Array(buffer_service.cols + 1).join('a'));
|
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
|
# params[1] left and above with wrap
|
||||||
# expect(buffer_service.buffer.lines.get(2)!.isWrapped).true;
|
# confirm precondition that line 2 is wrapped
|
||||||
# buffer_service.buffer.y = 2;
|
assert_true(buffer_service.buffer.lines.get_line(2).is_wrapped)
|
||||||
# buffer_service.buffer.x = 40;
|
buffer_service.buffer.y = 2
|
||||||
# input_handler.erase_in_display(Params.from_array([1]));
|
buffer_service.buffer.x = 40
|
||||||
# expect(buffer_service.buffer.lines.get(2)!.isWrapped).false;
|
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;
|
# reset and add a wrapped line
|
||||||
# buffer_service.buffer.x = 0;
|
buffer_service.buffer.y = 0
|
||||||
# input_handler.parse(Array(buffer_service.cols + 1).join('a')); # line 0
|
buffer_service.buffer.x = 0
|
||||||
# input_handler.parse(Array(buffer_service.cols + 10).join('a')); # line 1 and 2
|
input_handler.parse(repeat("a", buffer_service.cols)) # line 0
|
||||||
# for (let i = 3; i < buffer_service.rows; ++i) input_handler.parse(Array(buffer_service.cols + 1).join('a'));
|
input_handler.parse(repeat("a", buffer_service.cols + 9)) # line 1 and 2
|
||||||
#
|
for i in range(3, buffer_service.rows):
|
||||||
# # params[1] left and above with wrap
|
input_handler.parse(repeat("a", buffer_service.cols))
|
||||||
# # confirm precondition that line 2 is wrapped
|
|
||||||
# expect(buffer_service.buffer.lines.get(2)!.isWrapped).true;
|
# params[1] left and above with wrap
|
||||||
# buffer_service.buffer.y = 1;
|
# confirm precondition that line 2 is wrapped
|
||||||
# buffer_service.buffer.x = 90; # Cursor is beyond last column
|
assert_true(buffer_service.buffer.lines.get_line(2).is_wrapped)
|
||||||
# input_handler.erase_in_display(Params.from_array([1]));
|
buffer_service.buffer.y = 1
|
||||||
# expect(buffer_service.buffer.lines.get(2)!.isWrapped).false;
|
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():
|
func test_print_does_not_cause_an_infinite_loop():
|
||||||
|
|
|
@ -3,22 +3,27 @@
|
||||||
# License MIT
|
# License MIT
|
||||||
extends 'res://addons/gut/test.gd'
|
extends 'res://addons/gut/test.gd'
|
||||||
|
|
||||||
|
|
||||||
const Params = preload("res://addons/godot_xterm/parser/params.gd")
|
const Params = preload("res://addons/godot_xterm/parser/params.gd")
|
||||||
|
|
||||||
|
|
||||||
class TestParams:
|
class TestParams:
|
||||||
extends 'res://addons/gut/test.gd'
|
extends 'res://addons/gut/test.gd'
|
||||||
|
|
||||||
var params
|
var params
|
||||||
|
|
||||||
|
|
||||||
func before_each():
|
func before_each():
|
||||||
params = Params.new()
|
params = Params.new()
|
||||||
|
|
||||||
|
|
||||||
func test_respects_ctor_args():
|
func test_respects_ctor_args():
|
||||||
params = Params.new(12, 23)
|
params = Params.new(12, 23)
|
||||||
assert_eq(params.params.size(), 12)
|
assert_eq(params.params.size(), 12)
|
||||||
assert_eq(params.sub_params.size(), 23)
|
assert_eq(params.sub_params.size(), 23)
|
||||||
assert_eq(params.to_array(), [])
|
assert_eq(params.to_array(), [])
|
||||||
|
|
||||||
|
|
||||||
func test_add_param():
|
func test_add_param():
|
||||||
params.add_param(1)
|
params.add_param(1)
|
||||||
assert_eq(params.length, 1)
|
assert_eq(params.length, 1)
|
||||||
|
@ -30,6 +35,7 @@ class TestParams:
|
||||||
assert_eq(params.to_array(), [1, 23])
|
assert_eq(params.to_array(), [1, 23])
|
||||||
assert_eq(params.sub_params_length, 0)
|
assert_eq(params.sub_params_length, 0)
|
||||||
|
|
||||||
|
|
||||||
func test_add_sub_param():
|
func test_add_sub_param():
|
||||||
params.add_param(1)
|
params.add_param(1)
|
||||||
params.add_sub_param(2)
|
params.add_sub_param(2)
|
||||||
|
@ -43,6 +49,7 @@ class TestParams:
|
||||||
assert_eq(params.sub_params_length, 3)
|
assert_eq(params.sub_params_length, 3)
|
||||||
assert_eq(params.to_array(), [1, [2,3], 12345, [-1]])
|
assert_eq(params.to_array(), [1, [2,3], 12345, [-1]])
|
||||||
|
|
||||||
|
|
||||||
func test_should_not_add_sub_params_without_previous_param():
|
func test_should_not_add_sub_params_without_previous_param():
|
||||||
params.add_sub_param(2)
|
params.add_sub_param(2)
|
||||||
params.add_sub_param(3)
|
params.add_sub_param(3)
|
||||||
|
@ -56,6 +63,7 @@ class TestParams:
|
||||||
assert_eq(params.sub_params_length, 2)
|
assert_eq(params.sub_params_length, 2)
|
||||||
assert_eq(params.to_array(), [1, [2, 3]])
|
assert_eq(params.to_array(), [1, [2, 3]])
|
||||||
|
|
||||||
|
|
||||||
func test_reset():
|
func test_reset():
|
||||||
params.add_param(1)
|
params.add_param(1)
|
||||||
params.add_sub_param(2)
|
params.add_sub_param(2)
|
||||||
|
@ -94,11 +102,23 @@ class TestParams:
|
||||||
assert_eq(Params.from_array(data).to_array(), [12345, [-1]])
|
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:
|
class TestParse:
|
||||||
extends 'res://addons/gut/test.gd'
|
extends 'res://addons/gut/test.gd'
|
||||||
|
|
||||||
|
|
||||||
var params
|
var params
|
||||||
|
|
||||||
|
|
||||||
func parse(params, s):
|
func parse(params, s):
|
||||||
params.reset()
|
params.reset()
|
||||||
params.add_param(0)
|
params.add_param(0)
|
||||||
|
@ -125,17 +145,21 @@ class TestParse:
|
||||||
# End for
|
# End for
|
||||||
i+=1
|
i+=1
|
||||||
|
|
||||||
|
|
||||||
func before_each():
|
func before_each():
|
||||||
params = Params.new()
|
params = Params.new()
|
||||||
|
|
||||||
|
|
||||||
func test_param_defaults_to_0(): # ZDM (Zero Default Mode)
|
func test_param_defaults_to_0(): # ZDM (Zero Default Mode)
|
||||||
parse(params, '')
|
parse(params, '')
|
||||||
assert_eq(params.to_array(), [0])
|
assert_eq(params.to_array(), [0])
|
||||||
|
|
||||||
|
|
||||||
func test_sub_param_defaults_to_neg_1():
|
func test_sub_param_defaults_to_neg_1():
|
||||||
parse(params, ':')
|
parse(params, ':')
|
||||||
assert_eq(params.to_array(), [0, [-1]])
|
assert_eq(params.to_array(), [0, [-1]])
|
||||||
|
|
||||||
|
|
||||||
func test_reset_on_new_sequence():
|
func test_reset_on_new_sequence():
|
||||||
parse(params, '1;2;3')
|
parse(params, '1;2;3')
|
||||||
assert_eq(params.to_array(), [1, 2, 3])
|
assert_eq(params.to_array(), [1, 2, 3])
|
||||||
|
@ -146,6 +170,7 @@ class TestParse:
|
||||||
parse(params, '')
|
parse(params, '')
|
||||||
assert_eq(params.to_array(), [0])
|
assert_eq(params.to_array(), [0])
|
||||||
|
|
||||||
|
|
||||||
func test_should_handle_length_restrictions_correctly():
|
func test_should_handle_length_restrictions_correctly():
|
||||||
params = Params.new(3, 3)
|
params = Params.new(3, 3)
|
||||||
parse(params, '1;2;3')
|
parse(params, '1;2;3')
|
||||||
|
@ -163,6 +188,7 @@ class TestParse:
|
||||||
parse(params, '4;38:2::50:100:150;48:5:22')
|
parse(params, '4;38:2::50:100:150;48:5:22')
|
||||||
assert_eq(params.to_array(), [4, 38, [2, -1, 50], 48])
|
assert_eq(params.to_array(), [4, 38, [2, -1, 50], 48])
|
||||||
|
|
||||||
|
|
||||||
func test_typical_sequences():
|
func test_typical_sequences():
|
||||||
# SGR with semicolon syntax
|
# SGR with semicolon syntax
|
||||||
parse(params, '0;4;38;2;50;100;150;48;5;22')
|
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')
|
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]])
|
assert_eq(params.to_array(), [0, 4, 38, [2, -1, 50, 100, 150], 48, [5, 22]])
|
||||||
|
|
||||||
|
|
||||||
func test_clamp_parsed_params():
|
func test_clamp_parsed_params():
|
||||||
parse(params, '2147483648')
|
parse(params, '2147483648')
|
||||||
assert_eq(params.to_array(), [0x7FFFFFFF])
|
assert_eq(params.to_array(), [0x7FFFFFFF])
|
||||||
|
|
||||||
|
|
||||||
func test_clamp_parsed_sub_params():
|
func test_clamp_parsed_sub_params():
|
||||||
parse(params, ':2147483648')
|
parse(params, ':2147483648')
|
||||||
assert_eq(params.to_array(), [0, [0x7FFFFFFF]])
|
assert_eq(params.to_array(), [0, [0x7FFFFFFF]])
|
||||||
|
|
||||||
|
|
||||||
func test_should_cancel_subdigits_if_beyond_params_limit():
|
func test_should_cancel_subdigits_if_beyond_params_limit():
|
||||||
parse(params, ';;;;;;;;;10;;;;;;;;;;20;;;;;;;;;;30;31;32;33;34;35::::::::')
|
parse(params, ';;;;;;;;;10;;;;;;;;;;20;;;;;;;;;;30;31;32;33;34;35::::::::')
|
||||||
assert_eq(params.to_array(), [
|
assert_eq(params.to_array(), [
|
||||||
|
@ -190,13 +219,4 @@ class TestParse:
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 31, 32
|
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]])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue