Multiple changes

Former-commit-id: db8e674358
This commit is contained in:
Leroy Hopson 2020-09-29 16:16:59 +07:00
parent a55a05d3a4
commit 9bd17ec8dc
31 changed files with 5058 additions and 1031 deletions

View file

@ -1,64 +0,0 @@
tool
extends EditorPlugin
const Label3D = preload("label_3d.gd")
var converter_button : Button
var edited_node : Label3D
func _enter_tree():
yield(get_tree(), "idle_frame")
add_custom_type(
"Label3D", "Spatial",
Label3D,
preload("icon_label_3d.svg")
)
if not converter_button:
converter_button = preload("label_3d_converter.tscn").instance()
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, converter_button)
converter_button.connect("mesh_generated", self, "generate_mesh")
converter_button.hide()
print("3d text plugin added to project.")
func _exit_tree():
remove_custom_type("Label3D")
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, converter_button)
print("3d text plugin removed from project.")
func handles(object : Object) -> bool:
var handle = object is Label3D
if not handle:
converter_button.hide()
return handle
func edit(object):
edited_node = object
if edited_node is Label3D:
converter_button.show()
converter_button.label3d = object
else:
converter_button.hide()
func clear():
edited_node = null
converter_button.hide()
func generate_mesh(mesh_inst):
var undo_redo = get_undo_redo()
undo_redo.create_action("Convert Text")
undo_redo.add_do_method(edited_node.get_parent(), "add_child", mesh_inst)
undo_redo.add_undo_method(edited_node.get_parent(), "remove_child", mesh_inst)
undo_redo.commit_action()
mesh_inst.set_owner(get_editor_interface().get_edited_scene_root())

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Roujel Williams
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,47 +0,0 @@
class_name ArrayUtils
# Shifts the array's values by a specified amount.
# Positive amount shifts right while negative shifts left.
static func shift_array(array : Array, amount : int):
var right = bool(sign(amount) * 0.5 + 0.5)
for i in abs(amount):
var value
if right:
value = array.pop_back()
array.push_front(value)
else:
value = array.pop_front()
array.push_back(value)
# Combines the array with the other array.
static func append_array(array : Array, other : Array):
for i in other:
array.append(i)
# Splits the array between a and b at index.
static func split_array(array : Array, index : int, a : Array, b : Array):
for i in range(0, index):
a.append(array[i])
for i in range(index, array.size()):
b.append(array[i])
# Converts array to dictionary
static func to_dictionary(array : Array) -> Dictionary:
var dict = {}
for i in array.size():
dict[i] = array[i]
return dict
# Calls a function per object in the array with variable arguments.
static func call_per_element(array : Array, function : String, vars = null):
match typeof(vars):
TYPE_NIL:
for e in array:
e.call(function)
TYPE_ARRAY:
for e in array:
e.callv(function, vars)
_:
for e in array:
e.call(function, vars)

View file

@ -1,8 +0,0 @@
[gd_resource type="DynamicFont" load_steps=2 format=2]
[ext_resource path="res://addons/SIsilicon.3d.text/default_font.ttf" type="DynamicFontData" id=1]
[resource]
size = 100
use_filter = true
font_data = ExtResource( 1 )

View file

@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m6 3a1.0001 1.0001 0 0 0 -.70703.29297l-4 4a1.0001 1.0001 0 0 0 0 1.4141l4 4a1.0001 1.0001 0 0 0 .70703.29297h8a1.0001 1.0001 0 0 0 1-1v-8a1.0001 1.0001 0 0 0 -1-1h-8zm-1 4a1 1 0 0 1 1 1 1 1 0 0 1 -1 1 1 1 0 0 1 -1-1 1 1 0 0 1 1-1z" fill="#fc9c9c" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 367 B

View file

@ -1,34 +0,0 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/icon_label_3d.svg-66ebb0fa17446da110a5ed153702e3c3.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/SIsilicon.3d.text/icon_label_3d.svg"
dest_files=[ "res://.import/icon_label_3d.svg-66ebb0fa17446da110a5ed153702e3c3.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0

View file

@ -1,148 +0,0 @@
tool
extends Spatial
export(String, MULTILINE) var text = "Text" setget set_text
export(float) var text_scale = 0.01 setget set_text_scale
export(float) var extrude = 0.0 setget set_extrude
export(Font) var font setget set_font;
export(int, "Left", "Right", "Center", "Fill") var align setget set_align
export(Color) var color = Color(0.6, 0.6, 0.6) setget set_color
export(float, 0, 1) var metallic = 0.0 setget set_metallic
export(float, 0, 1) var roughness = 0.5 setget set_roughness
export(int) var max_steps = 256 setget set_max_steps
export(float) var step_size = 1.0 setget set_step_size
var label
var viewport
var proxy
var material
func _ready():
for i in range(get_child_count()):
remove_child(get_child(0))
viewport = preload("text_viewport.tscn").instance()
label = viewport.get_node("Label")
add_child(viewport)
proxy = MeshInstance.new()
proxy.mesh = CubeMesh.new()
proxy.material_override = preload("label_3d.material").duplicate()
material = proxy.material_override
var view_texture = viewport.get_texture()
view_texture.flags = Texture.FLAG_FILTER
material.set_shader_param("text", view_texture)
add_child(proxy)
set_align(align)
set_font(font)
set_text(text)
set_text_scale(text_scale)
set_extrude(extrude)
set_color(color)
set_metallic(metallic)
set_roughness(roughness)
set_max_steps(max_steps)
set_step_size(step_size)
func set_text(string):
text = string;
if label:
label.text = text
label.rect_size = Vector2()
label.force_update_transform()
var size = label.rect_size
viewport.size = size
viewport.render_target_update_mode = Viewport.UPDATE_ALWAYS
yield(get_tree(), "idle_frame")
label.rect_size = Vector2()
label.force_update_transform()
size = label.rect_size
viewport.size = size
yield(get_tree(), "idle_frame")
viewport.render_target_update_mode = Viewport.UPDATE_DISABLED
proxy.scale.x = size.x * text_scale
proxy.scale.y = size.y * text_scale
func set_text_scale(scale):
text_scale = scale
if label:
var size = label.rect_size
if proxy:
proxy.scale.x = size.x * text_scale
proxy.scale.y = size.y * text_scale
func set_extrude(ext):
extrude = ext
if proxy:
proxy.scale.z = extrude if extrude != 0 else 1
material.set_shader_param("extrude", extrude != 0)
if extrude == 0 and proxy.mesh is CubeMesh:
proxy.mesh = QuadMesh.new()
proxy.mesh.size = Vector2(2, 2)
elif proxy.mesh is QuadMesh:
proxy.mesh = CubeMesh.new()
func set_font(f):
font = f
if label:
if font:
label.add_font_override("font", font)
else:
label.add_font_override("font", preload("default_font.tres"))
set_text(text)
func set_align(al):
align = al
if label:
match align:
0:
label.align = Label.ALIGN_LEFT
1:
label.align = Label.ALIGN_RIGHT
2:
label.align = Label.ALIGN_CENTER
3:
label.align = Label.ALIGN_FILL
set_text(text)
func set_color(col):
color = col
if material:
material.set_shader_param("albedo", color)
func set_metallic(metal):
metallic = metal
if material:
material.set_shader_param("metallic", metallic)
func set_roughness(rough):
roughness = rough
if material:
material.set_shader_param("roughness", roughness)
func set_max_steps(max_s):
max_steps = max(max_s, 8)
if material:
material.set_shader_param("maxSteps", max_steps)
func set_step_size(step_s):
step_size = max(step_s, 0)
if material:
material.set_shader_param("stepSize", step_size)

View file

@ -1,461 +0,0 @@
tool
extends Button
signal mesh_generated(mesh_inst)
const Label3D = preload("label_3d.gd")
var label3d : Label3D
var image_cache = PoolRealArray()
# These variables are used to spread the marching_square function
# across multiple frames.
var finished_marching = false
var os_time
func _on_Button_pressed():
generate_geometry()
func generate_geometry():
$PopupDialog.popup_centered()
var text_scale = label3d.text_scale
var extrude = label3d.extrude
var viewport = label3d.get_node("Viewport")
viewport.render_target_update_mode = Viewport.UPDATE_ALWAYS
yield(get_tree(), "idle_frame")
yield(get_tree(), "idle_frame")
viewport.render_target_update_mode = Viewport.UPDATE_DISABLED
var image = viewport.get_texture().get_data()
image.lock()
var edges = Dictionary()
finished_marching = false
os_time = OS.get_ticks_msec()
do_marching_squares(image, edges)
while not finished_marching:
yield(get_tree(), "idle_frame")
# Contours are the edges and holes of the text.
# The paths are said edges combined with the holes for easier triangulation.
var contours = []
collect_contours(edges.duplicate(), contours)
var paths = contours.duplicate(true)
decimate_holes(edges, paths, Rect2(0, 0, image.get_width(), image.get_height()))
var triangle_data = []
var vertex_data = []
for path in paths:
path.points = douglas_peucker(path.points, 0.125)
# Edge data beyond this point will no longer be valid
var triangles = []
ear_clipping(path, triangles)
# points are converted into vector3 here.
for p in path.points.size():
var point = path.points[p]
point -= Vector2(image.get_width(), image.get_height()) / 2.0
point *= text_scale * 2.0
path.points[p] = Vector3(point.x, point.y, extrude)
ArrayUtils.call_per_element(triangles, "offset_indices", vertex_data.size())
ArrayUtils.append_array(vertex_data, path.points)
ArrayUtils.append_array(triangle_data, triangles)
if extrude != 0:
# Generate the back triangles
var original_vert_size = vertex_data.size()
for v in original_vert_size:
var vertex = vertex_data[v]
vertex_data.append(vertex * Vector3(1, 1, -1))
var tri_size = triangle_data.size()
for t in tri_size:
var triangle = triangle_data[t].duplicate()
triangle.offset_indices(original_vert_size)
triangle.reverse_order()
triangle_data.append(triangle)
# And now the triangles inbetween
for path in paths:
var vertices = path.points.duplicate()
var points_size = path.points.size()
for vert in points_size:
var back_vertex = path.points[vert] * Vector3(1, 1, -1)
vertices.append(back_vertex)
var triangles = []
for i in points_size:
var index = i + vertex_data.size()
var a = index
var b = (index + 1) if i != points_size - 1 else vertex_data.size()
var c = index + points_size
var d = ((index + 1) if i != points_size - 1 else vertex_data.size()) + points_size
triangles.append(Triangle.new(c, b, a))
triangles.append(Triangle.new(d, b, c))
ArrayUtils.append_array(vertex_data, vertices)
ArrayUtils.append_array(triangle_data, triangles)
var geom = SurfaceTool.new()
geom.clear()
geom.begin(Mesh.PRIMITIVE_TRIANGLES)
for vert in vertex_data:
geom.add_vertex(vert)
for tri in triangle_data:
geom.add_index(tri.a)
geom.add_index(tri.b)
geom.add_index(tri.c)
geom.generate_normals()
var material = SpatialMaterial.new()
material.albedo_color = label3d.color
material.metallic = label3d.metallic
material.roughness = label3d.roughness
var mesh_inst = MeshInstance.new()
mesh_inst.mesh = geom.commit()
mesh_inst.material_override = material
mesh_inst.transform = label3d.transform
mesh_inst.name = label3d.name + "-mesh"
emit_signal("mesh_generated", mesh_inst)
image_cache.resize(0)
image.unlock()
$PopupDialog.hide()
func do_marching_squares(image, edges):
for y in image.get_height() - 1:
for x in image.get_width() - 1:
var i = Vector2(x, y)
var p_ul = get_pixel(image, x, y)
var p_ll = get_pixel(image, x, y+1)
var p_ur = get_pixel(image, x+1, y)
var p_lr = get_pixel(image, x+1, y+1)
var top = inverse_lerp(p_ul, p_ur, 1)
var bottom = inverse_lerp(p_ll, p_lr, 1)
var left = inverse_lerp(p_ul, p_ll, 1)
var right = inverse_lerp(p_ur, p_lr, 1)
var ul = int(p_ul > 1) * 1
var ll = int(p_ll > 1) * 2
var ur = int(p_ur > 1) * 4
var lr = int(p_lr > 1) * 8
var bit = ul | ll | ur | lr
# Notice: cases 6 and 9 have not been implemented.
match bit:
# Corner Cases
1:
create_edge(edges, i, x, y + left, x + top, y, Vector2.UP)
2:
create_edge(edges, i, x + bottom, y + 1, x, y + left, Vector2.LEFT)
4:
create_edge(edges, i, x + top, y, x + 1, y + right, Vector2.RIGHT)
8:
create_edge(edges, i, x + 1, y + right, x + bottom, y + 1, Vector2.DOWN)
# Edge Cases
3:
create_edge(edges, i, x + bottom, y + 1, x + top, y, Vector2.UP)
5:
create_edge(edges, i, x, y + left, x + 1, y + right, Vector2.RIGHT)
10:
create_edge(edges, i, x + 1, y + right, x, y + left, Vector2.LEFT)
12:
create_edge(edges, i, x + top, y, x + bottom, y + 1, Vector2.DOWN)
# Inner Corner cases
14:
create_edge(edges, i, x + top, y, x, y + left, Vector2.LEFT)
13:
create_edge(edges, i, x, y + left, x + bottom, y + 1, Vector2.DOWN)
11:
create_edge(edges, i, x + 1, y + right, x + top, y, Vector2.UP)
7:
create_edge(edges, i, x + bottom, y + 1, x + 1, y + right, Vector2.RIGHT)
if OS.get_ticks_msec() - os_time > 20:
var max_i = (image.get_width() - 1) * (image.get_height() - 1)
var curr_i = x + (y * image.get_width() - 1)
$PopupDialog/VBoxContainer/ProgressBar.value = float(curr_i) / max_i * 100
yield(get_tree(), "idle_frame")
os_time = OS.get_ticks_msec()
finished_marching = true
func collect_contours(edges, contours):
var directions = [Vector2.UP, Vector2.DOWN, Vector2.LEFT, Vector2.RIGHT]
var coord = edges.keys()[0]
var start_edge = edges[coord]
var contour = Contour.new(start_edge.direction == Vector2.UP || \
start_edge.direction == Vector2.RIGHT)
contours.append(contour)
contour.points.append(edges[coord].end)
contour.edges.append(edges[coord])
var edge = edges[coord]
edges.erase(coord)
var loop_started = true
while loop_started or edge != start_edge:
var edge_found = false
for dir in directions:
if edges.has(coord + dir) and edges[coord + dir].begin == edge.end:
edge_found = true
coord += dir
contour.points.append(edges[coord].end)
contour.edges.append(edges[coord])
edge = edges[coord]
edges.erase(coord)
break
if not edge_found:
break
loop_started = false
if not edges.empty():
collect_contours(edges, contours)
func decimate_holes(edges, contours, bounds):
for c in range(contours.size()-1, -1, -1):
var contour = contours[c]
if contour.is_hole:
for i in contour.edges.size():
var edge = contour.edges[i]
var edge_found = false
var dir = edge.direction
dir = Vector2(dir.y, -dir.x)
var pos = ((edge.begin + edge.end) / 2.0).floor()
var other_edge
while not other_edge and bounds.has_point(pos):
pos += dir
if edges.has(pos):
other_edge = edges[pos]
if other_edge:
for other_contour in contours:
if other_contour == contour:
continue
for j in other_contour.edges.size():
var other_point = other_contour.edges[j]
if other_edge == other_point:
edge_found = true
other_contour.fuse_with(contour, j, i + 1)
break
if edge_found:
break
if edge_found:
break
contours.remove(c)
func douglas_peucker(points, tolerance):
var farthest = farthest_point(points)
# Farthest point not existing must mean the points only made of two,
# and cannot be simplified any further.
if not farthest:
return points
if farthest.distance < tolerance:
return [points[0], points[-1]]
else:
var left = []
var right = []
ArrayUtils.split_array(points, farthest.index, left, right)
left = douglas_peucker(left, tolerance)
right = douglas_peucker(right, tolerance)
ArrayUtils.append_array(left, right)
return left
func ear_clipping(contour, triangles):
var points = ArrayUtils.to_dictionary(contour.points)
var i = 0
var counter = 0
while points.size() > 3:
counter += 1
if(counter > 4000):
printerr("Hmmm... Infinite loop much?")
break
var keys = points.keys()
var point_a = points[keys[i-1]]
var point_b = points[keys[i]]
var point_c = points[keys[(i+1) % keys.size()]]
if (point_b - point_a).cross(point_c - point_b) < 0:
var has_point = false
for p in points:
var point = points[p]
if point == point_a or point == point_b or point == point_c:
continue
if point_inside_triangle(point, point_a, point_b, point_c):
has_point = true
break
if not has_point:
var tri = Triangle.new(keys[i-1], keys[i], keys[(i+1) % keys.size()])
triangles.append(tri)
points.erase(keys[i])
i = wrapi(i + 1, 0, points.size())
var keys = points.keys()
triangles.append(Triangle.new(keys[0], keys[1], keys[2]))
# This returns a dictionary containing the farthest point,
# the distance associated with it, and its index in the array.
func farthest_point(points):
var first = points[0]
var last = points[-1]
if points.size() < 3:
return
var farthest
var max_dist = -1
var index
for i in range(1, points.size() - 1):
var distance = distance_to_segment(points[i], first, last)
if distance > max_dist:
farthest = points[i]
max_dist = distance
index = i
return {"point": farthest, "distance": max_dist, "index": index}
func distance_to_segment(point, a, b):
# This is because I don't know how to snap a point onto a line,
# so I'm relying on Plane in the meantime.
var plane = Plane(Vector3(a.x, a.y, 0),
Vector3(b.x, b.y, 0), Vector3(a.x, a.y, 1))
var projected = plane.project(Vector3(point.x, point.y, 0))
var t = inverse_lerp(a.x, b.x, projected.x) if a.x != b.x else \
inverse_lerp(a.y, b.y, projected.y)
var snapped = a.linear_interpolate(b, t)
return point.distance_squared_to(snapped)
func vec_sign(p1, p2, p3):
return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
func point_inside_triangle(point, a, b, c):
var d1 = vec_sign(point, a, b)
var d2 = vec_sign(point, b, c)
var d3 = vec_sign(point, c, a)
var has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
var has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)
return not (has_neg and has_pos)
func create_edge(edges, offset, p1x, p1y, p2x, p2y, dir):
edges[offset] = MSEdge.new(Vector2(p1x, p1y), Vector2(p2x, p2y), dir)
func get_pixel(image : Image, x : int, y : int):
if x < 1 || y < 1 || x > image.get_width()-1 || y > image.get_height()-1:
return 0
var i = x + y * image.get_width()
if image_cache.size() > i and image_cache[i] != null:
return image_cache[i]
else:
var real = image.get_pixel(x, y).r;
if image_cache.size() <= i:
image_cache.resize(i+1)
image_cache[i] = real
return real
class MSEdge:
var begin : Vector2
var end : Vector2
var coordinate : Vector2
# One of the four cardinal directions
var direction : Vector2
func _init(begin, end, direction):
self.begin = begin
self.end = end
self.direction = direction
class Contour:
var points := []
var edges := []
var is_hole : bool
func _init(is_hole):
self.is_hole = is_hole
func fuse_with(contour, self_point, other_point):
var points_behind = []
var points_after = []
ArrayUtils.split_array(points, self_point + 1, points_behind, points_after)
var shifted_contour = contour.points.duplicate()
ArrayUtils.shift_array(shifted_contour, other_point)
ArrayUtils.append_array(points_behind, shifted_contour)
points_behind.append(shifted_contour[0])
points_behind.append(points[self_point])
ArrayUtils.append_array(points_behind, points_after)
points = points_behind
class Triangle:
var a : int
var b : int
var c : int
func _init(a, b, c):
self.a = a
self.b = b
self.c = c
func offset_indices(offset):
a += offset
b += offset
c += offset
func reverse_order():
var temp = a
a = b
b = temp
func duplicate():
return Triangle.new(a, b, c)

View file

@ -1,40 +0,0 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://addons/SIsilicon.3d.text/label_3d_converter.gd" type="Script" id=1]
[node name="Button" type="Button"]
margin_right = 172.0
margin_bottom = 20.0
text = "Convert to MeshInstance"
flat = true
script = ExtResource( 1 )
[node name="PopupDialog" type="PopupDialog" parent="."]
visible = true
margin_left = 340.0
margin_top = 200.0
margin_right = 610.0
margin_bottom = 268.0
mouse_default_cursor_shape = 5
popup_exclusive = true
[node name="VBoxContainer" type="VBoxContainer" parent="PopupDialog"]
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 6.0
margin_top = 6.0
margin_right = -6.0
margin_bottom = -6.0
[node name="Label" type="Label" parent="PopupDialog/VBoxContainer"]
margin_right = 258.0
margin_bottom = 14.0
text = "Generating geometry"
align = 1
[node name="ProgressBar" type="ProgressBar" parent="PopupDialog/VBoxContainer"]
margin_top = 18.0
margin_right = 258.0
margin_bottom = 32.0
step = 1.0
[connection signal="pressed" from="." to="." method="_on_Button_pressed"]

View file

@ -1,7 +0,0 @@
[plugin]
name="3D Text"
description="This plugin allows you to make 3D text within the Godot engine."
author="SIsilicon"
version="0.6.1"
script="3d_text_plugin.gd"

View file

@ -1,74 +0,0 @@
[gd_scene load_steps=7 format=2]
[ext_resource path="res://addons/SIsilicon.3d.text/default_font.ttf" type="DynamicFontData" id=1]
[sub_resource type="Environment" id=1]
background_mode = 1
background_energy = 0.0
[sub_resource type="World" id=2]
environment = SubResource( 1 )
[sub_resource type="DynamicFont" id=3]
size = 100
font_data = ExtResource( 1 )
[sub_resource type="Shader" id=4]
code = "shader_type canvas_item;
uniform int Max = 3;
float getAlpha(sampler2D tex, ivec2 coord) {
return texelFetch(tex, coord, 0).r;
}
void fragment() {
ivec2 coord = ivec2(FRAGCOORD.xy);
bool inside = getAlpha(SCREEN_TEXTURE, coord) > 0.5;
float dist = float((Max+1) * (Max+1));
for(int y = -Max; y <= Max; y++) {
for(int x = -Max; x <= Max; x++) {
if(x == 0 && y == 0) continue;
ivec2 delta = ivec2(x,y);
if(inside) {
if(getAlpha(SCREEN_TEXTURE, coord + delta) < 0.5) {
dist = min(dist, float(x*x + y*y));
}
} else {
if(getAlpha(SCREEN_TEXTURE, coord + delta) > 0.5) {
dist = min(dist, float(x*x + y*y));
}
}
}
}
dist = sqrt(dist) * (float(!inside) * 2.0 - 1.0);
COLOR = vec4(dist / float(Max+1));
}"
[sub_resource type="ShaderMaterial" id=5]
shader = SubResource( 4 )
shader_param/Max = 10
[node name="Viewport" type="Viewport"]
size = Vector2( 190, 114 )
own_world = true
world = SubResource( 2 )
transparent_bg = true
handle_input_locally = false
render_target_update_mode = 1
gui_disable_input = true
[node name="Label" type="Label" parent="."]
margin_right = 190.0
margin_bottom = 114.0
size_flags_horizontal = 4
custom_fonts/font = SubResource( 3 )
text = "Text"
[node name="ColorRect" type="ColorRect" parent="Label"]
material = SubResource( 5 )
anchor_right = 1.0
anchor_bottom = 1.0

View file

@ -45,7 +45,13 @@ func import(source_file, save_path, options, r_platform_variant, r_gen_files):
asciicast.add_track(Animation.TYPE_METHOD, 0)
asciicast.track_set_path(0, ".")
var time: float
var frame = {
"time": 0.0,
"data": {
"method": "write",
"args": [PoolByteArray()]
}
}
while not file.eof_reached():
var line = file.get_line()
@ -56,14 +62,25 @@ func import(source_file, save_path, options, r_platform_variant, r_gen_files):
if typeof(p.result) != TYPE_ARRAY:
continue
time = p.result[0]
var event_type: String = p.result[1]
var event_data: PoolByteArray = p.result[2].to_utf8()
if event_type == "o":
asciicast.track_insert_key(0, time, {"method": "write",
"args": [event_data]})
# Asciicast recordings have a resolution of 0.000001, however animation
# track keys only have a resolution of 0.01, therefore we must combine
# events that would occur in the same keyframe, otherwise only the last
# event is inserted and the previous events are overwritten.
var time = stepify(p.result[0], 0.01)
asciicast.length = time
if event_type == "o":
if time == frame.time:
asciicast.track_remove_key_at_position(0, time)
frame.data.args[0] = frame.data.args[0] + event_data
else:
frame.time = time
frame.data.args = [event_data]
asciicast.track_insert_key(0, frame.time, frame.data)
asciicast.length = frame.time
return ResourceSaver.save("%s.%s" % [save_path, get_save_extension()], asciicast)

View file

@ -1,6 +1,7 @@
#include "pseudoterminal.h"
#include <pty.h>
#include <unistd.h>
#include <sys/wait.h>
#include <termios.h>
using namespace godot;
@ -11,9 +12,11 @@ void Pseudoterminal::_register_methods()
register_method("_init", &Pseudoterminal::_init);
register_method("_ready", &Pseudoterminal::_ready);
register_method("put_data", &Pseudoterminal::put_data);
register_method("write", &Pseudoterminal::write);
register_method("resize", &Pseudoterminal::resize);
register_signal<Pseudoterminal>((char *)"data_received", "data", GODOT_VARIANT_TYPE_POOL_BYTE_ARRAY);
register_signal<Pseudoterminal>((char *)"data_sent", "data", GODOT_VARIANT_TYPE_POOL_BYTE_ARRAY);
register_signal<Pseudoterminal>((char *)"exited", "status", GODOT_VARIANT_TYPE_INT);
}
Pseudoterminal::Pseudoterminal()
@ -22,18 +25,20 @@ Pseudoterminal::Pseudoterminal()
Pseudoterminal::~Pseudoterminal()
{
pty_thread.join();
}
void Pseudoterminal::_init()
{
pty_thread = std::thread(&Pseudoterminal::process_pty, this);
bytes_to_write = 0;
pty_thread = std::thread(&Pseudoterminal::process_pty, this);
}
void Pseudoterminal::process_pty()
{
int fd;
char *name;
int status;
should_process_pty = true;
@ -68,9 +73,30 @@ void Pseudoterminal::process_pty()
}
else
{
Vector2 zero = Vector2(0, 0);
/* Parent */
while (1)
{
{
std::lock_guard<std::mutex> guard(size_mutex);
if (size != zero)
{
struct winsize ws;
memset(&ws, 0, sizeof(ws));
ws.ws_col = size.x;
ws.ws_row = size.y;
ioctl(fd, TIOCSWINSZ, &ws);
}
}
if (waitpid(pty_pid, &status, WNOHANG))
{
emit_signal("exited", status);
return;
}
int ready = -1;
fd_set read_fds;
fd_set write_fds;
@ -93,10 +119,7 @@ void Pseudoterminal::process_pty()
if (bytes_to_write > 0)
{
write(fd, write_buffer, bytes_to_write);
Godot::print(String("wrote {0} bytes").format(Array::make(bytes_to_write)));
::write(fd, write_buffer, bytes_to_write);
bytes_to_write = 0;
}
}
@ -114,30 +137,11 @@ void Pseudoterminal::process_pty()
if (bytes_read <= 0)
continue;
//while (1)
//{
// ret = read(fd, read_buffer, 1);
// if (ret == -1 || ret == 0)
// {
// break;
// }
// else
// {
// bytes_read += ret;
// }
//}
PoolByteArray data = PoolByteArray();
data.resize(bytes_read);
memcpy(data.write().ptr(), read_buffer, bytes_read);
emit_signal("data_received", PoolByteArray(data));
if (bytes_read > 0)
{
//Godot::print(String("read {0} bytes").format(Array::make(bytes_read)));
}
emit_signal("data_sent", PoolByteArray(data));
}
}
}
@ -148,9 +152,15 @@ void Pseudoterminal::_ready()
{
}
void Pseudoterminal::put_data(PoolByteArray data)
void Pseudoterminal::write(PoolByteArray data)
{
std::lock_guard<std::mutex> guard(write_buffer_mutex);
bytes_to_write = data.size();
memcpy(write_buffer, data.read().ptr(), bytes_to_write);
}
void Pseudoterminal::resize(Vector2 new_size)
{
std::lock_guard<std::mutex> guard(size_mutex);
size = new_size;
}

View file

@ -29,6 +29,9 @@ namespace godot
int bytes_to_read;
std::mutex read_buffer_mutex;
Vector2 size;
std::mutex size_mutex;
void process_pty();
public:
@ -40,7 +43,8 @@ namespace godot
void _init();
void _ready();
void put_data(PoolByteArray data);
void write(PoolByteArray data);
void resize(Vector2 size);
};
} // namespace godot

View file

@ -215,7 +215,17 @@ static void write_cb(struct tsm_vte *vte, const char *u8, size_t len, void *data
for (int i = 0; i < len; i++)
bytes.append(u8[i]);
term->emit_signal("data_read", bytes);
if (len > 0)
{
if (term->input_event_key.is_valid())
{
// The callback was fired from a key press event so emit the "key_pressed" signal.
term->emit_signal("key_pressed", String(u8), term->input_event_key);
term->input_event_key.unref();
}
term->emit_signal("data_sent", bytes);
}
}
static int text_draw_cb(struct tsm_screen *con,
@ -258,7 +268,6 @@ static int text_draw_cb(struct tsm_screen *con,
void Terminal::_register_methods()
{
register_method("_init", &Terminal::_init);
register_method("_ready", &Terminal::_ready);
register_method("_gui_input", &Terminal::_gui_input);
@ -267,10 +276,12 @@ void Terminal::_register_methods()
register_method("write", &Terminal::write);
register_method("update_size", &Terminal::update_size);
//register_property<Terminal, int>("rows", &Terminal::rows, 24);
//register_property<Terminal, int>("cols", &Terminal::cols, 80);
register_property<Terminal, int>("rows", &Terminal::rows, 24);
register_property<Terminal, int>("cols", &Terminal::cols, 80);
register_signal<Terminal>("data_read", "data", GODOT_VARIANT_TYPE_POOL_BYTE_ARRAY);
register_signal<Terminal>("data_sent", "data", GODOT_VARIANT_TYPE_POOL_BYTE_ARRAY);
register_signal<Terminal>("key_pressed", "data", GODOT_VARIANT_TYPE_STRING, "event", GODOT_VARIANT_TYPE_OBJECT);
register_signal<Terminal>("size_changed", "new_size", GODOT_VARIANT_TYPE_VECTOR2);
}
Terminal::Terminal()
@ -349,6 +360,7 @@ void Terminal::_gui_input(Variant event)
auto iter = keymap.find({unicode, scancode});
uint32_t keysym = (iter != keymap.end() ? iter->second : XKB_KEY_NoSymbol);
input_event_key = k;
tsm_vte_handle_keyboard(vte, keysym, ascii, mods, unicode ? unicode : TSM_VTE_INVALID);
}
}
@ -425,6 +437,9 @@ void Terminal::draw_foreground(int row, int col, Color fgcolor)
struct cell cell = cells[row][col];
if (cell.ch == nullptr)
return; // No foreground to draw
/* Set the font */
Ref<Font> fontref = get_font("");
@ -448,9 +463,6 @@ void Terminal::draw_foreground(int row, int col, Color fgcolor)
/* Draw the foreground */
if (cell.ch == nullptr)
return; // No foreground to draw
if (cell.attr.blink)
; // TODO: Handle blink
@ -523,7 +535,7 @@ void Terminal::update_size()
rows = std::max(2, (int)floor(get_rect().size.y / cell_size.y));
cols = std::max(1, (int)floor(get_rect().size.x / cell_size.x));
Godot::print(String("resized_rows: {0}, resized_cols: {1}").format(Array::make(rows, cols)));
emit_signal("size_changed", Vector2(cols, rows));
Cells new_cells = {};

View file

@ -28,6 +28,8 @@ namespace godot
Cells cells;
Ref<InputEventKey> input_event_key;
protected:
tsm_screen *screen;
tsm_vte *vte;

View file

@ -0,0 +1,131 @@
# Terminal
**Inherits:** [Control] < [CanvasItem] < [Node] < [Object]
Terminal emulator.
**IMPORTANT:**
<img align="right" src="./docs/important_properties.png"/>
- If you are not seeing anything in the terminal check that a theme has been set. If there is no theme, everything will be drawn in black by default.
- If the terminal isn't responding to keyboard or mouse input check that `focus_mode` is set to `All`, otherwise `_gui_input()` won't be called so no input will be processed.
## Description
![Flow Diagram](./docs/flow_diagram.svg)
### (1) User Input
The users enters some data into the terminal, typically by typing something on the keyboard or clicking (and possibly dragging) somewhere on the screen.
This corresponds to the `_gui_input()` method which is implemented in [terminal.cpp](../native/src/terminal.cpp).
### (2) Terminal Output
The user input from (1) is processed by the terminal and converted.
For example, if the user were to press the down key, the terminal would
and the `data_sent` signal would be emitted with the value `"\u001b[A"`.
For a full list of escape sequences ["XTerm Control Sequences"](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html).
### (3) Terminal Input
In the other direction, characters can be sent to the terminal. This corresponds to the `write()` method.
### (4) Draw
The input from (3) is then intepreted by the terminal and drawn to the screen.
For example if the string "\u001b[39;m" was written to the terminal, then it would draw some colorful output to the screen.
## Properties
| Type | Name | Default |
|-------|------|---------|
| [int] | rows | 24 |
| [int] | cols | 80 |
## Methods
## Theme Properties
| Type | Name | Default |
|---------|-------------------------------|------------------------------|
| [Color] | Terminal/colors/Background | Color(0.0, 0.0, 0.0, 1.0) |
| [Color] | Terminal/colors/Black | Color(0.0, 0.0, 0.0, 1.0) |
| [Color] | Terminal/colors/Blue | Color(0.0, 0.0, 0.5, 1.0) |
| [Color] | Terminal/colors/Cyan | Color(0.0, 0.5, 0.5, 1.0) |
| [Color] | Terminal/colors/Dark Grey | Color(0.5, 0.5, 0.5, 1.0) |
| [Color] | Terminal/colors/Foreground | Color(1.0, 1.0, 1.0, 1.0) |
| [Color] | Terminal/colors/Green | Color(0.0, 0.5, 0.0, 1.0) |
| [Color] | Terminal/colors/Light Blue | Color(0.0, 0.0, 1.0, 1.0) |
| [Color] | Terminal/colors/Light Cyan | Color(0.0, 1.0, 1.0, 1.0) |
| [Color] | Terminal/colors/Light Green | Color(0.0, 1.0, 0.0, 1.0) |
| [Color] | Terminal/colors/Light Grey | Color(0.75, 0.75, 0.75, 1.0) |
| [Color] | Terminal/colors/Light Magenta | Color(1.0, 0.0, 1.0, 1.0) |
| [Color] | Terminal/colors/Light Red | Color(1.0, 0.0, 0.0, 1.0) |
| [Color] | Terminal/colors/Light Yellow | Color(1.0, 1.0, 0.0, 1.0) |
| [Color] | Terminal/colors/Magenta | Color(0.5, 0.0, 0.5, 1.0) |
| [Color] | Terminal/colors/Red | Color(0.5, 0.0, 0.0, 1.0) |
| [Color] | Terminal/colors/White | Color(1.0, 1.0, 1.0, 1.0) |
| [Color] | Terminal/colors/Yellow | Color(0.5, 0.5, 0.0, 1.0) |
| [Font] | Terminal/fonts/Bold | |
| [Font] | Terminal/fonts/Bold Italic | |
| [Font] | Terminal/fonts/Italic | |
| [Font] | Terminal/fonts/Regular | |
## Signals
- **data_sent** **(** PoolByteArray data **)**
Emitted when some data comes out of the terminal.
This typically occurs when the user interacts with the terminal by typing on the keyboard or clicking somewhere.
It might also occur.
Depending on several terminal settings can be interpreted differently, depending on modifier keys and the terminals settings/state.
In a typical setup, this data would be forwarded to the linux operating system.
---
- **key_pressed** **(** int row **)**
Emitted when a key is pressed.
## Property Descriptions
- [int] **rows**
| | |
|-----------|-----------------|
| *Default* | 24 |
| *Setter* | set_rows(value) |
| *Getter* | get_rows(value) |
The number of rows in the terminal's rect.
When using a monospace font, this is typically the number of characters that can fit from one side to another.
Therefore it is affected by the things thing.
---
- [int] **cols**
The number of columns in the terminal's rect.
## Method Descriptions
- void **write** **(** String|PoolByteArray data **)**
Writes data to the terminal emulator. Accepts either a String or PoolByteArray.
Example:
```gdscript
$Terminal.write("Hello World")
$Terminal.write("Hello World".to_utf8())
$Terminal.write(PoolByteArray([0x1b, 0x9e])
```
[Control]: https://docs.godotengine.org/en/stable/classes/class_control.html#class-control
[CanvasItem]: https://docs.godotengine.org/en/stable/classes/class_canvasitem.html#class-canvasitem
[Node]: https://docs.godotengine.org/en/stable/classes/class_node.html#class-node
[Object]: https://docs.godotengine.org/en/stable/classes/class_object.html#class-object
[Color]: https://docs.godotengine.org/en/stable/classes/class_color.html#class-color
[Font]: https://docs.godotengine.org/en/stable/classes/class_font.html#class-font
[int]: https://docs.godotengine.org/en/stable/classes/class_int.html#class-int

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -17,3 +17,7 @@ func get_class() -> String:
func is_class(name) -> bool:
return name == get_class() or .is_class(name)
func _init():
step = 0.01 # Parent override.

View file

@ -0,0 +1,66 @@
extends Reference
class_name TPut
# Control Sequence Introducer
const CSI = "\u001b["
const CURSOR_UP = "\u001b[A"
const CURSOR_DOWN = "\u001b[B"
const CURSOR_RIGHT = "\u001b[C"
const CURSOR_LEFT = "\u001b[D"
const DEFAULT_FOREGROUND_COLOR = "\u001b[0m"
var terminal
func _init(p_terminal: Control) -> void:
if p_terminal:
terminal = p_terminal
func write_string(string: String, color: Color = Color.white) -> void:
if color:
var fg = "\u001b[38;2;%d;%d;%dm" % [color.r8, color.g8, color.b8]
terminal.write(fg.to_utf8())
terminal.write(string.to_utf8())
# Reset color back to default.
terminal.write("\u001b[0m".to_utf8())
# tput_* functions based on the tput command.
# See: https://man7.org/linux/man-pages/man1/tput.1.html for more info.
# Hide the cursor.
func civis():
terminal.write("%s?25l" % CSI)
# Position the cursor at the given row and col.
func cup(row: int = 0, col: int = 0) -> void:
terminal.write("\u001b[%d;%dH" % [row, col])
func setaf(color: Color) -> void:
var fg = "\u001b[38;2;%d;%d;%dm" % [color.r8, color.g8, color.b8]
terminal.write(fg)
func setab(color: Color) -> void:
var bg = "\u001b[48;2;%d;%d;%dm" % [color.r8, color.g8, color.b8]
terminal.write(bg)
func rev() -> void:
terminal.write("\u001b[7m")
func sgr0() -> void:
terminal.write("\u001b[0m")
func reset() -> void:
terminal.write("\u001bc")

View file

@ -1,24 +1,23 @@
[gd_scene load_steps=5 format=2]
[gd_scene load_steps=4 format=2]
[ext_resource path="res://examples/asciicast/asciicast.gd" type="Script" id=1]
[ext_resource path="res://addons/godot_xterm/themes/default.theme" type="Theme" id=2]
[ext_resource path="res://addons/godot_xterm/nodes/terminal/terminal.gdns" type="Script" id=4]
[ext_resource path="res://examples/asciicast/example.cast" type="Animation" id=6]
[node name="Container" type="Container"]
margin_right = 40.0
margin_bottom = 40.0
script = ExtResource( 1 )
[node name="Terminal" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
focus_mode = 2
theme = ExtResource( 2 )
script = ExtResource( 4 )
__meta__ = {
"_edit_use_anchors_": false
}
rows = 31
cols = 102
[node name="Terminal" type="Control" parent="."]
margin_right = 40.0
margin_bottom = 40.0
theme = ExtResource( 2 )
script = ExtResource( 4 )
[node name="AnimationPlayer" type="AnimationPlayer" parent="Terminal"]
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
autoplay = "example"
playback_speed = 2.0
method_call_mode = 1
anims/a = ExtResource( 6 )
anims/example = ExtResource( 6 )

View file

@ -1,61 +1,779 @@
{"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"]
{"version": 2, "width": 95, "height": 56, "timestamp": 1601879053, "env": {"SHELL": "/run/current-system/sw/bin/bash", "TERM": "xterm"}}
[0.009015, "o", "sh-4.4$ "]
[0.768572, "o", "#"]
[0.994511, "o", " "]
[1.251891, "o", "P"]
[1.390056, "o", "r"]
[1.556055, "o", "e"]
[1.633343, "o", "s"]
[1.758128, "o", "s"]
[1.837416, "o", " "]
[2.082763, "o", "E"]
[2.15487, "o", "S"]
[2.206575, "o", "C"]
[2.535156, "o", " "]
[2.675284, "o", "t"]
[2.764107, "o", "o"]
[2.826769, "o", " "]
[2.95468, "o", "e"]
[3.107351, "o", "x"]
[3.236995, "o", "i"]
[3.315722, "o", "t"]
[3.363186, "o", " "]
[3.464495, "o", "t"]
[3.502985, "o", "h"]
[3.561326, "o", "i"]
[3.637971, "o", "s"]
[3.696915, "o", " "]
[3.828476, "o", "d"]
[3.920953, "o", "e"]
[3.977016, "o", "m"]
[4.080181, "o", "o"]
[4.183719, "o", " "]
[4.411889, "o", "a"]
[4.505969, "o", "t"]
[4.602301, "o", " "]
[4.738613, "o", "a"]
[4.815285, "o", "n"]
[4.970011, "o", "y"]
[5.048946, "o", " "]
[5.140987, "o", "t"]
[5.22994, "o", "i"]
[5.300908, "o", "m"]
[5.391697, "o", "e"]
[5.833247, "o", "\r\n"]
[5.833292, "o", "sh-4.4$ "]
[6.497175, "o", "#"]
[6.584674, "o", " "]
[6.783908, "o", "T"]
[6.982053, "o", "h"]
[7.075839, "o", "i"]
[7.121841, "o", "s"]
[7.207194, "o", " "]
[7.472273, "o", "t"]
[7.56542, "o", "e"]
[7.633672, "o", "r"]
[7.708612, "o", "m"]
[7.794029, "o", "i"]
[7.852906, "o", "n"]
[7.960908, "o", "a"]
[8.034148, "o", "l"]
[8.1316, "o", " "]
[8.2592, "o", "s"]
[8.376712, "o", "e"]
[8.453216, "o", "s"]
[8.583006, "o", "s"]
[8.64767, "o", "i"]
[8.711495, "o", "o"]
[8.779941, "o", "n"]
[8.862385, "o", " "]
[9.112564, "o", "w"]
[9.236889, "o", "a"]
[9.29605, "o", "s"]
[9.376424, "o", " "]
[9.672264, "o", "r"]
[9.768395, "o", "e"]
[9.875859, "o", "c"]
[10.360921, "o", "o"]
[10.48178, "o", "r"]
[10.573511, "o", "d"]
[10.70553, "o", "e"]
[10.772014, "o", "d"]
[10.901859, "o", " "]
[11.222266, "o", "u"]
[11.314773, "o", "s"]
[11.388137, "o", "i"]
[11.462257, "o", "n"]
[11.508588, "o", "g"]
[11.610203, "o", " "]
[11.704458, "o", "t"]
[11.769386, "o", "h"]
[11.829674, "o", "e"]
[11.920249, "o", " "]
[12.057327, "o", "a"]
[12.150311, "o", "s"]
[12.221965, "o", "c"]
[12.335971, "o", "i"]
[12.461568, "o", "i"]
[12.659298, "o", "n"]
[12.787306, "o", "e"]
[12.861305, "o", "m"]
[13.243252, "o", "a"]
[13.415869, "o", " "]
[13.640678, "o", "p"]
[13.727956, "o", "r"]
[13.79999, "o", "o"]
[13.869981, "o", "g"]
[13.932686, "o", "r"]
[14.120489, "o", "a"]
[14.563155, "o", "m"]
[14.950597, "o", "\r\n"]
[14.950636, "o", "sh-4.4$ "]
[15.25787, "o", "#"]
[15.357041, "o", " "]
[15.549955, "o", "F"]
[15.69595, "o", "o"]
[15.742517, "o", "r"]
[15.816029, "o", " "]
[16.017346, "o", "m"]
[16.134482, "o", "o"]
[16.214998, "o", "r"]
[16.287911, "o", "e"]
[16.341746, "o", " "]
[16.536357, "o", "i"]
[16.63003, "o", "n"]
[16.757694, "o", "f"]
[16.840006, "o", "o"]
[16.955199, "o", " "]
[17.10089, "o", "v"]
[17.212207, "o", "i"]
[17.310707, "o", "s"]
[17.433414, "o", "i"]
[17.504056, "o", "t"]
[17.639282, "o", " "]
[18.027938, "o", "h"]
[18.148473, "o", "t"]
[18.254587, "o", "t"]
[18.365992, "o", "p"]
[18.422981, "o", "s"]
[18.69077, "o", ":"]
[18.981756, "o", "/"]
[19.072248, "o", "/"]
[19.451925, "o", "a"]
[19.52595, "o", "s"]
[19.590197, "o", "c"]
[19.690475, "o", "i"]
[19.812762, "o", "i"]
[20.002006, "o", "n"]
[20.285781, "o", "e"]
[20.436427, "o", "m"]
[20.528716, "o", "a"]
[20.763135, "o", "."]
[20.903967, "o", "o"]
[20.975119, "o", "r"]
[20.985324, "o", "g"]
[20.99331, "o", "\r\nsh-4.4$ "]
[21.605944, "o", "#"]
[21.807285, "o", " "]
[22.010063, "o", "L"]
[22.188024, "o", "e"]
[22.279903, "o", "t"]
[22.361474, "o", "'"]
[22.486357, "o", "s"]
[22.57465, "o", " "]
[22.740959, "o", "i"]
[22.813009, "o", "n"]
[22.857842, "o", "s"]
[22.954863, "o", "t"]
[23.066967, "o", "a"]
[23.150687, "o", "l"]
[23.263568, "o", "l"]
[23.34495, "o", " "]
[24.24858, "o", "a"]
[24.339416, "o", "s"]
[24.418355, "o", "c"]
[24.541209, "o", "i"]
[24.697872, "o", "i"]
[24.872434, "o", "n"]
[24.964613, "o", "e"]
[25.07858, "o", "m"]
[25.153616, "o", "a"]
[26.123044, "o", "\r\n"]
[26.123082, "o", "sh-4.4$ "]
[26.610637, "o", "n"]
[26.731314, "o", "i"]
[26.840493, "o", "x"]
[26.950531, "o", "-"]
[27.030416, "o", "e"]
[27.143202, "o", "nv "]
[27.380791, "o", "-"]
[27.494532, "o", "i"]
[27.766916, "o", "A"]
[27.947392, "o", " "]
[28.069982, "o", "n"]
[28.153264, "o", "i"]
[28.285577, "o", "x"]
[28.398953, "o", "o"]
[28.528587, "o", "s"]
[28.714452, "o", "."]
[29.066201, "o", "a"]
[29.178501, "o", "s"]
[29.257687, "o", "c"]
[29.36677, "o", "i"]
[29.487456, "o", "i"]
[29.617427, "o", "n"]
[29.708982, "o", "e"]
[29.793394, "o", "m"]
[29.885927, "o", "a"]
[30.166746, "o", "\r\n"]
[30.269591, "o", "installing 'asciinema-2.0.2'\r\n"]
[30.442995, "o", "these paths will be fetched (0.05 MiB download, 0.19 MiB unpacked):\r\n /nix/store/jn1kalsv6g21rzhr46wsmb33rlbnxbp2-asciinema-2.0.2\r\n"]
[30.443793, "o", "copying path '/nix/store/jn1kalsv6g21rzhr46wsmb33rlbnxbp2-asciinema-2.0.2' from 'https://aseipp-nix-cache.global.ssl.fastly.net'...\r\n"]
[30.772997, "o", "building '/nix/store/j8zk1ixrzz8ffl2r8bfcblrbp8jib14n-user-environment.drv'...\r\n"]
[30.827776, "o", "created 2238 symlinks in user environment\r\n"]
[30.837521, "o", "\r\nsh-4.4$ "]
[31.439756, "o", "#"]
[31.531759, "o", " "]
[31.733672, "o", "N"]
[31.885784, "o", "o"]
[31.969473, "o", "w"]
[32.04399, "o", " "]
[32.184525, "o", "t"]
[32.498387, "o", "\b\u001b[K"]
[32.674218, "o", "w"]
[32.780078, "o", "e"]
[32.869318, "o", " "]
[33.015693, "o", "c"]
[33.131168, "o", "a"]
[33.215492, "o", "n"]
[33.277638, "o", " "]
[33.435631, "o", "r"]
[33.491613, "o", "e"]
[33.769703, "o", "c"]
[33.878877, "o", "o"]
[33.964167, "o", "r"]
[34.051787, "o", "d"]
[34.231502, "o", " "]
[34.38911, "o", "a"]
[34.517173, "o", " "]
[34.660129, "o", "n"]
[34.75175, "o", "e"]
[34.83474, "o", "w"]
[34.908203, "o", " "]
[36.304364, "o", "s"]
[36.387211, "o", "e"]
[36.465211, "o", "s"]
[36.596101, "o", "s"]
[36.695781, "o", "i"]
[36.762876, "o", "o"]
[36.824392, "o", "n"]
[36.922221, "o", " "]
[37.108339, "o", "t"]
[37.264521, "o", "o"]
[37.361574, "o", " "]
[37.490987, "o", "a"]
[37.578879, "o", " "]
[38.018804, "o", "."]
[38.110959, "o", "c"]
[38.210231, "o", "a"]
[38.270978, "o", "s"]
[38.3198, "o", "t"]
[38.402376, "o", " "]
[38.566701, "o", "f"]
[38.660991, "o", "i"]
[38.73289, "o", "l"]
[38.813984, "o", "e"]
[39.342188, "o", "\r\n"]
[39.342365, "o", "sh-4.4$ "]
[39.885874, "o", "a"]
[39.959473, "o", "s"]
[40.034948, "o", "c"]
[40.133815, "o", "i"]
[40.215169, "o", "inema "]
[40.590943, "o", "r"]
[40.672976, "o", "e"]
[40.752245, "o", "c"]
[40.83314, "o", " "]
[41.861795, "o", "n"]
[41.938138, "o", "e"]
[42.032994, "o", "w"]
[42.144281, "o", "."]
[42.250499, "o", "c"]
[42.323403, "o", "a"]
[42.407712, "o", "s"]
[42.470832, "o", "t"]
[42.814799, "o", "\r\n"]
[42.90735, "o", "\u001b[0;32masciinema: recording asciicast to new.cast\u001b[0m\r\n\u001b[0;32masciinema: press <ctrl-d> or type \"exit\" when you're done\u001b[0m\r\n"]
[42.951473, "o", "\u001b]2;laptop:leroy:~/projects/godot-xterm\u0007\r\r\n\u001b[1;32m[\u001b]0;leroy@laptop: ~/projects/godot-xterm\u0007leroy@laptop:~/projects/godot-xterm]$\u001b[0m "]
[45.647452, "o", "l"]
[45.943288, "o", "s"]
[46.286881, "o", "\r\n"]
[46.288383, "o", "\u001b[0m\u001b[01;34maddons\u001b[0m docker-compose.yml example.cast \u001b[01;35micon.png\u001b[0m new.cast\r\nCHANGELOG.md \u001b[01;34mdockerfiles\u001b[0m example.cast.import icon.png.import project.godot\r\ndefault_env.tres \u001b[01;34mdocs\u001b[0m \u001b[01;34mexamples\u001b[0m LICENSE README.md\r\n"]
[46.288974, "o", "\u001b]2;laptop:leroy:~/projects/godot-xterm\u0007\r\r\n\u001b[1;32m[\u001b]0;leroy@laptop: ~/projects/godot-xterm\u0007leroy@laptop:~/projects/godot-xterm]$\u001b[0m "]
[48.264058, "o", "s"]
[48.35698, "o", "h"]
[48.465218, "o", "a"]
[48.860943, "o", "1"]
[49.137467, "o", "s"]
[49.255023, "o", "u"]
[49.402646, "o", "m"]
[49.994215, "o", " "]
[50.137741, "o", "."]
[50.251377, "o", "/"]
[50.478669, "o", "*"]
[51.035519, "o", " "]
[51.587166, "o", "|"]
[51.713609, "o", " "]
[51.863023, "o", "l"]
[51.96232, "o", "o"]
[52.012303, "o", "l"]
[52.240101, "o", "c"]
[52.353223, "o", "a"]
[52.447717, "o", "t"]
[52.565175, "o", " "]
[52.947413, "o", "-"]
[53.353261, "o", "F"]
[53.634436, "o", " "]
[53.936746, "o", "0"]
[54.099132, "o", "."]
[54.364446, "o", "3"]
[54.66516, "o", "\r\n"]
[54.666593, "o", "sha1sum: ./addons: Is a directory\r\n"]
[54.66663, "o", "sha1sum: ./dockerfiles: Is a directory\r\nsha1sum: ./docs: Is a directory\r\nsha1sum: ./examples: Is a directory\r\n"]
[54.819505, "o", "\u001b[38;2;180;1;202mb\u001b[39m\u001b[38;2;168;3;211m2\u001b[39m\u001b[38;2;156;6;221mf\u001b[39m"]
[54.819607, "o", "\u001b[38;2;143;10;229m7\u001b[39m\u001b[38;2;131;16;236m9\u001b[39m\u001b[38;2;118;23;242me\u001b[39m"]
[54.819638, "o", "\u001b[38;2;105;30;247m8\u001b[39m\u001b[38;2;93;39;251mb\u001b[39m\u001b[38;2;81;48;253m2\u001b[39m\u001b[38;2;69;59;254mc\u001b[39m\u001b[38;2;58;70;254m2\u001b[39m"]
[54.819747, "o", "\u001b[38;2;48;81;253m9\u001b[39m\u001b[38;2;39;93;251m1\u001b[39m"]
[54.819806, "o", "\u001b[38;2;30;106;247ma\u001b[39m\u001b[38;2;22;118;242m7\u001b[39m"]
[54.819936, "o", "\u001b[38;2;16;131;236m9\u001b[39m\u001b[38;2;10;144;228m2\u001b[39m\u001b[38;2;6;156;220m7\u001b[39m\u001b[38;2;3;168;211m4\u001b[39m\u001b[38;2;1;180;201m3\u001b[39m"]
[54.820001, "o", "\u001b[38;2;1;191;191m2\u001b[39m\u001b[38;2;1;202;179m2\u001b[39m"]
[54.820056, "o", "\u001b[38;2;3;212;167m2\u001b[39m\u001b[38;2;6;221;155md\u001b[39m\u001b[38;2;11;229;143m8"]
[54.82013, "o", "\u001b[39m\u001b[38;2;16;236;130md\u001b[39m\u001b[38;2;23;242;117m4\u001b[39m\u001b[38;2;31;247;105m7\u001b[39m"]
[54.820187, "o", "\u001b[38;2;39;251;92me\u001b[39m\u001b[38;2;49;253;80mc\u001b[39m"]
[54.820245, "o", "\u001b[38;2;59;254;69m2\u001b[39m\u001b[38;2;70;254;58m5\u001b[39m"]
[54.820314, "o", "\u001b[38;2;82;253;48me\u001b[39m\u001b[38;2;94;250;38m8\u001b[39m\u001b[38;2;106;246;30m1"]
[54.820376, "o", "\u001b[39m\u001b[38;2;119;241;22m3\u001b[39m\u001b[38;2;132;235;15ma\u001b[39m"]
[54.820439, "o", "\u001b[38;2;144;228;10m5\u001b[39m\u001b[38;2;157;220;6m2\u001b[39m\u001b[38;2;169;211;3m3\u001b[39m"]
[54.820504, "o", "\u001b[38;2;181;201;1m \u001b[39m\u001b[38;2;192;190;1m \u001b[39m"]
[54.820559, "o", "\u001b[38;2;203;179;1m.\u001b[39m\u001b[38;2;213;167;3m/\u001b[39m"]
[54.82062, "o", "\u001b[38;2;222;154;7mC\u001b[39m\u001b[38;2;230;142;11mH\u001b[39m\u001b[38;2;237;129;17mA\u001b[39m"]
[54.820678, "o", "\u001b[38;2;243;117;23mN\u001b[39m\u001b[38;2;247;104;31mG\u001b[39m"]
[54.820738, "o", "\u001b[38;2;251;92;40mE\u001b[39m\u001b[38;2;253;80;50mL\u001b[39m"]
[54.820795, "o", "\u001b[38;2;254;68;60mO\u001b[39m\u001b[38;2;254;57;71mG\u001b[39m\u001b[38;2;253;47;83m.\u001b[39m"]
[54.820855, "o", "\u001b[38;2;250;38;95mm\u001b[39m\u001b[38;2;246;29;107md\u001b[39m"]
[54.820926, "o", "\u001b[38;2;241;22;120m\u001b[39m\r\n"]
[54.820985, "o", "\u001b[38;2;143;10;229m8\u001b[39m"]
[54.821038, "o", "\u001b[38;2;131;16;236m0\u001b[39m\u001b[38;2;118;23;242m4\u001b[39m\u001b[38;2;105;30;247m2\u001b[39m"]
[54.821094, "o", "\u001b[38;2;93;39;251m5\u001b[39m\u001b[38;2;81;48;253m3\u001b[39m"]
[54.821149, "o", "\u001b[38;2;69;59;254mf\u001b[39m\u001b[38;2;58;70;254m5\u001b[39m"]
[54.821206, "o", "\u001b[38;2;48;81;253m2\u001b[39m\u001b[38;2;39;93;251m6\u001b[39m"]
[54.821258, "o", "\u001b[38;2;30;106;247m6\u001b[39m\u001b[38;2;22;118;242m5\u001b[39m"]
[54.821311, "o", "\u001b[38;2;16;131;236mc\u001b[39m"]
[54.821381, "o", "\u001b[38;2;10;144;228m6\u001b[39m"]
[54.821436, "o", "\u001b[38;2;6;156;220m8\u001b[39m\u001b[38;2;3;168;211m7\u001b[39m\u001b[38;2;1;180;201md\u001b[39m"]
[54.821486, "o", "\u001b[38;2;1;191;191m6\u001b[39m\u001b[38;2;1;202;179m6\u001b[39m\u001b[38;2;3;212;167mb\u001b[39m"]
[54.821577, "o", "\u001b[38;2;6;221;155m4\u001b[39m\u001b[38;2;11;229;143ma\u001b[39m\u001b[38;2;16;236;130m2\u001b[39m\u001b[38;2;23;242;117m4\u001b[39m\u001b[38;2;31;247;105mb\u001b[39m"]
[54.821633, "o", "\u001b[38;2;39;251;92m3\u001b[39m\u001b[38;2;49;253;80m9\u001b[39m"]
[54.82166, "o", "\u001b[38;2;59;254;69mf\u001b[39m"]
[54.821741, "o", "\u001b[38;2;70;254;58m1\u001b[39m\u001b[38;2;82;253;48mc\u001b[39m\u001b[38;2;94;250;38m3\u001b[39m"]
[54.821906, "o", "\u001b[38;2;106;246;30m2\u001b[39m\u001b[38;2;119;241;22m8\u001b[39m\u001b[38;2;132;235;15m6\u001b[39m\u001b[38;2;144;228;10m9\u001b[39m\u001b[38;2;157;220;6m6\u001b[39m"]
[54.822031, "o", "\u001b[38;2;169;211;3m7\u001b[39m"]
[54.822114, "o", "\u001b[38;2;181;201;1mc\u001b[39m"]
[54.822971, "o", "\u001b[38;2;192;190;1m9\u001b[39m"]
[54.82305, "o", "\u001b[38;2;203;179;1md\u001b[39m\u001b[38;2;213;167;3m \u001b[39m\u001b[38;2;222;154;7m \u001b[39m\u001b[38;2;230;142;11m.\u001b[39m"]
[54.823129, "o", "\u001b[38;2;237;129;17m/\u001b[39m\u001b[38;2;243;117;23md\u001b[39m"]
[54.823188, "o", "\u001b[38;2;247;104;31me\u001b[39m"]
[54.823241, "o", "\u001b[38;2;251;92;40mf\u001b[39m"]
[54.823306, "o", "\u001b[38;2;253;80;50ma\u001b[39m\u001b[38;2;254;68;60mu\u001b[39m"]
[54.823368, "o", "\u001b[38;2;254;57;71ml\u001b[39m"]
[54.823436, "o", "\u001b[38;2;253;47;83mt\u001b[39m\u001b[38;2;250;38;95m_\u001b[39m"]
[54.823503, "o", "\u001b[38;2;246;29;107me\u001b[39m\u001b[38;2;241;22;120mn\u001b[39m"]
[54.823562, "o", "\u001b[38;2;235;15;132mv\u001b[39m\u001b[38;2;228;10;145m.\u001b[39m"]
[54.823622, "o", "\u001b[38;2;219;6;158mt\u001b[39m"]
[54.82368, "o", "\u001b[38;2;210;3;170mr\u001b[39m\u001b[38;2;200;1;182me\u001b[39m"]
[54.82374, "o", "\u001b[38;2;189;1;193ms\u001b[39m\u001b[38;2;178;1;203m\u001b[39m\r\n"]
[54.823865, "o", "\u001b[38;2;105;30;247ma\u001b[39m"]
[54.823926, "o", "\u001b[38;2;93;39;251ma\u001b[39m\u001b[38;2;81;48;253ma\u001b[39m\u001b[38;2;69;59;254m3\u001b[39m"]
[54.823989, "o", "\u001b[38;2;58;70;254md\u001b[39m\u001b[38;2;48;81;253ma\u001b[39m"]
[54.824055, "o", "\u001b[38;2;39;93;251me\u001b[39m\u001b[38;2;30;106;247m7\u001b[39m"]
[54.824148, "o", "\u001b[38;2;22;118;242ma\u001b[39m\u001b[38;2;16;131;236m2\u001b[39m"]
[54.824208, "o", "\u001b[38;2;10;144;228m9\u001b[39m\u001b[38;2;6;156;220m8\u001b[39m"]
[54.824275, "o", "\u001b[38;2;3;168;211me\u001b[39m\u001b[38;2;1;180;201m9\u001b[39m"]
[54.824347, "o", "\u001b[38;2;1;191;191m5\u001b[39m\u001b[38;2;1;202;179m8\u001b[39m"]
[54.824414, "o", "\u001b[38;2;3;212;167m4\u001b[39m\u001b[38;2;6;221;155m1\u001b[39m"]
[54.824469, "o", "\u001b[38;2;11;229;143md\u001b[39m\u001b[38;2;16;236;130m8\u001b[39m"]
[54.824557, "o", "\u001b[38;2;23;242;117m4\u001b[39m\u001b[38;2;31;247;105m7\u001b[39m\u001b[38;2;39;251;92m5\u001b[39m"]
[54.824618, "o", "\u001b[38;2;49;253;80ma\u001b[39m\u001b[38;2;59;254;69mc\u001b[39m"]
[54.824683, "o", "\u001b[38;2;70;254;58m2\u001b[39m\u001b[38;2;82;253;48m3\u001b[39m"]
[54.824738, "o", "\u001b[38;2;94;250;38mb\u001b[39m"]
[54.824796, "o", "\u001b[38;2;106;246;30m8\u001b[39m\u001b[38;2;119;241;22ma\u001b[39m"]
[54.824852, "o", "\u001b[38;2;132;235;15me\u001b[39m\u001b[38;2;144;228;10mc\u001b[39m"]
[54.82491, "o", "\u001b[38;2;157;220;6m7\u001b[39m\u001b[38;2;169;211;3ma"]
[54.825026, "o", "\u001b[39m\u001b[38;2;181;201;1mb\u001b[39m\u001b[38;2;192;190;1mb\u001b[39m\u001b[38;2;203;179;1mb\u001b[39m"]
[54.825088, "o", "\u001b[38;2;213;167;3ma\u001b[39m\u001b[38;2;222;154;7ma\u001b[39m"]
[54.825155, "o", "\u001b[38;2;230;142;11m2\u001b[39m\u001b[38;2;237;129;17m \u001b[39m\u001b[38;2;243;117;23m \u001b[39m"]
[54.825218, "o", "\u001b[38;2;247;104;31m.\u001b[39m\u001b[38;2;251;92;40m/\u001b[39m"]
[54.825294, "o", "\u001b[38;2;253;80;50md\u001b[39m"]
[54.825351, "o", "\u001b[38;2;254;68;60mo\u001b[39m\u001b[38;2;254;57;71mc\u001b[39m"]
[54.825374, "o", "\u001b[38;2;253;47;83mk\u001b[39m"]
[54.825398, "o", "\u001b[38;2;250;38;95me\u001b[39m"]
[54.825502, "o", "\u001b[38;2;246;29;107mr\u001b[39m\u001b[38;2;241;22;120m-\u001b[39m\u001b[38;2;235;15;132mc\u001b[39m"]
[54.82557, "o", "\u001b[38;2;228;10;145mo\u001b[39m\u001b[38;2;219;6;158mm\u001b[39m"]
[54.825634, "o", "\u001b[38;2;210;3;170mp\u001b[39m\u001b[38;2;200;1;182mo\u001b[39m"]
[54.825699, "o", "\u001b[38;2;189;1;193ms\u001b[39m\u001b[38;2;178;1;203me\u001b[39m"]
[54.825832, "o", "\u001b[38;2;166;3;213m.\u001b[39m"]
[54.825889, "o", "\u001b[38;2;154;7;222my\u001b[39m"]
[54.825917, "o", "\u001b[38;2;141;11;230mm\u001b[39m"]
[54.825938, "o", "\u001b[38;2;129;17;237ml\u001b[39m"]
[54.826017, "o", "\u001b[38;2;116;24;243m\u001b[39m\r\n"]
[54.826105, "o", "\u001b[38;2;69;59;254m2\u001b[39m\u001b[38;2;58;70;254m8\u001b[39m"]
[54.826172, "o", "\u001b[38;2;48;81;253m8\u001b[39m\u001b[38;2;39;93;251mb\u001b[39m"]
[54.826245, "o", "\u001b[38;2;30;106;247m0\u001b[39m\u001b[38;2;22;118;242me\u001b[39m"]
[54.826317, "o", "\u001b[38;2;16;131;236m"]
[54.826382, "o", "4\u001b[39m\u001b[38;2;10;144;228md\u001b[39m"]
[54.826442, "o", "\u001b[38;2;6;156;220mb\u001b[39m"]
[54.826501, "o", "\u001b[38;2;3;168;211m3\u001b[39m\u001b[38;2;1;180;201m2\u001b[39m\u001b[38;2;1;191;191m1\u001b[39m"]
[54.826559, "o", "\u001b[38;2;1;202;179mb\u001b[39m"]
[54.826618, "o", "\u001b[38;2;3;212;167m8\u001b[39m"]
[54.826676, "o", "\u001b[38;2;6;221;155m3\u001b[39m\u001b[38;2;11;229;143mb\u001b[39m"]
[54.826734, "o", "\u001b[38;2;16;236;130m0\u001b[39m\u001b[38;2;23;242;117m1\u001b[39m\u001b[38;2;31;247;105m2"]
[54.826792, "o", "\u001b[39m\u001b[38;2;39;251;92m6"]
[54.826849, "o", "\u001b[39m\u001b[38;2;49;253;80m3\u001b[39m\u001b[38;2;59;254;69m0\u001b[39m"]
[54.826908, "o", "\u001b[38;2;70;254;58md\u001b[39m"]
[54.826965, "o", "\u001b[38;2;82;253;48m1\u001b[39m\u001b[38;2;94;250;38md\u001b[39m"]
[54.827023, "o", "\u001b[38;2;106;246;30md\u001b[39m\u001b[38;2;119;241;22m3\u001b[39m"]
[54.827081, "o", "\u001b[38;2;132;235;15m7\u001b[39m"]
[54.82714, "o", "\u001b[38;2;144;228;10mb\u001b[39m\u001b[38;2;157;220;6me\u001b[39m"]
[54.827212, "o", "\u001b[38;2;169;211;3m2\u001b[39m"]
[54.827282, "o", "\u001b[38;2;181;201;1m3\u001b[39m"]
[54.827347, "o", "\u001b[38;2;192;190;1m9\u001b[39m\u001b[38;2;203;179;1m9\u001b[39m\u001b[38;2;213;167;3ma\u001b[39m"]
[54.827406, "o", "\u001b[38;2;222;154;7m8\u001b[39m"]
[54.827466, "o", "\u001b[38;2;230;142;11m9\u001b[39m\u001b[38;2;237;129;17m6\u001b[39m"]
[54.827525, "o", "\u001b[38;2;243;117;23m4\u001b[39m\u001b[38;2;247;104;31m7\u001b[39m"]
[54.827584, "o", "\u001b[38;2;251;92;40m \u001b[39m"]
[54.827643, "o", "\u001b[38;2;253;80;50m \u001b[39m"]
[54.827701, "o", "\u001b[38;2;254;68;60m.\u001b[39m\u001b[38;2;254;57;71m/\u001b[39m"]
[54.827762, "o", "\u001b[38;2;253;47;83me\u001b[39m\u001b[38;2;250;38;95mx\u001b[39m"]
[54.827821, "o", "\u001b[38;2;246;29;107ma\u001b[39m\u001b[38;2;241;22;120mm"]
[54.827881, "o", "\u001b[39m\u001b[38;2;235;15;132mp\u001b[39m\u001b[38;2;228;10;145m"]
[54.82794, "o", "l\u001b[39m\u001b[38;2;219;6;158m"]
[54.827998, "o", "e\u001b[39m\u001b[38;2;210;3;170m.\u001b[39m\u001b[38;2;200;1;182mc"]
[54.828058, "o", "\u001b[39m\u001b[38;2;189;1;193ma\u001b[39m"]
[54.828118, "o", "\u001b[38;2;178;1;203ms\u001b[39m"]
[54.82864, "o", "\u001b[38;2;166;3;213mt\u001b[39m"]
[54.828711, "o", "\u001b[38;2;154;7;222m\u001b[39m\r\n"]
[54.828773, "o", "\u001b[38;2;39;93;251m5\u001b[39m"]
[54.828835, "o", "\u001b[38;2;30;106;247md\u001b[39m\u001b[38;2;22;118;242m5"]
[54.828897, "o", "\u001b[39m\u001b[38;2;16;131;236m1\u001b[39m\u001b[38;2;10;144;228md\u001b[39m"]
[54.828954, "o", "\u001b[38;2;6;156;220m3\u001b[39m"]
[54.829012, "o", "\u001b[38;2;3;168;211m9\u001b[39m"]
[54.829075, "o", "\u001b[38;2;1;180;201m8\u001b[39m\u001b[38;2;1;191;191mf\u001b[39m"]
[54.829137, "o", "\u001b[38;2;1;202;179m4\u001b[39m"]
[54.829213, "o", "\u001b[38;2;3;212;167m7\u001b[39m\u001b[38;2;6;221;155m1\u001b[39m"]
[54.829279, "o", "\u001b[38;2;11;229;143md\u001b[39m"]
[54.829412, "o", "\u001b[38;2;16;236;130m8\u001b[39m"]
[54.829498, "o", "\u001b[38;2;23;242;117ma\u001b[39m"]
[54.829565, "o", "\u001b[38;2;31;247;105ma\u001b[39m"]
[54.829626, "o", "\u001b[38;2;39;251;92me\u001b[39m"]
[54.829695, "o", "\u001b[38;2;49;253;80m2\u001b[39m\u001b[38;2;59;254;69m7\u001b[39m"]
[54.829729, "o", "\u001b[38;2;70;254;58mf\u001b[39m\u001b[38;2;82;253;48m0\u001b[39m"]
[54.829814, "o", "\u001b[38;2;94;250;38m0\u001b[39m"]
[54.829875, "o", "\u001b[38;2;106;246;30m8\u001b[39m\u001b[38;2;119;241;22mf\u001b[39m\u001b[38;2;132;235;15mf\u001b[39m"]
[54.829931, "o", "\u001b[38;2;144;228;10m9\u001b[39m\u001b[38;2;157;220;6m3\u001b[39m"]
[54.829981, "o", "\u001b[38;2;169;211;3m8\u001b[39m"]
[54.830032, "o", "\u001b[38;2;181;201;1m1\u001b[39m"]
[54.830082, "o", "\u001b[38;2;192;190;1m8\u001b[39m\u001b[38;2;203;179;1md\u001b[39m"]
[54.830141, "o", "\u001b[38;2;213;167;3m2\u001b[39m\u001b[38;2;222;154;7mc"]
[54.830201, "o", "\u001b[39m\u001b[38;2;230;142;11m4\u001b[39m\u001b[38;2;237;129;17m5\u001b[39m"]
[54.830254, "o", "\u001b[38;2;243;117;23m6\u001b[39m"]
[54.830316, "o", "\u001b[38;2;247;104;31m5\u001b[39m\u001b[38;2;251;92;40md\u001b[39m"]
[54.830371, "o", "\u001b[38;2;253;80;50m6\u001b[39m"]
[54.830401, "o", "\u001b[38;2;254;68;60m9\u001b[39m"]
[54.83049, "o", "\u001b[38;2;254;57;71m \u001b[39m\u001b[38;2;253;47;83m \u001b[39m\u001b[38;2;250;38;95m."]
[54.830522, "o", "\u001b[39m"]
[54.830611, "o", "\u001b[38;2;246;29;107m/\u001b[39m\u001b[38;2;241;22;120me\u001b[39m\u001b[38;2;235;15;132mx"]
[54.830665, "o", "\u001b[39m\u001b[38;2;228;10;145ma\u001b[39m"]
[54.830719, "o", "\u001b[38;2;219;6;158mm\u001b[39m"]
[54.830767, "o", "\u001b[38;2;210;3;170mp\u001b[39m"]
[54.830816, "o", "\u001b[38;2;200;1;182ml\u001b[39m\u001b[38;2;189;1;193me"]
[54.830869, "o", "\u001b[39m\u001b[38;2;178;1;203m.\u001b[39m\u001b[38;2;166;3;213mc\u001b[39m"]
[54.830919, "o", "\u001b[38;2;154;7;222ma\u001b[39m"]
[54.830972, "o", "\u001b[38;2;141;11;230ms\u001b[39m"]
[54.831023, "o", "\u001b[38;2;129;17;237mt\u001b[39m\u001b[38;2;116;24;243m.\u001b[39m"]
[54.831079, "o", "\u001b[38;2;103;32;248mi\u001b[39m"]
[54.831107, "o", "\u001b[38;2;91;40;251mm\u001b[39m"]
[54.831169, "o", "\u001b[38;2;79;50;253mp\u001b[39m\u001b[38;2;68;61;254m"]
[54.831239, "o", "o\u001b[39m\u001b[38;2;57;72;254mr\u001b[39m\u001b[38;2;46;83;253mt\u001b[39m"]
[54.831306, "o", "\u001b[38;2;37;95;250m\u001b[39m\r\n"]
[54.831393, "o", "\u001b[38;2;16;131;236mf\u001b[39m"]
[54.83146, "o", "\u001b[38;2;10;144;228m5\u001b[39m"]
[54.831523, "o", "\u001b[38;2;6;156;220m7\u001b[39m\u001b[38;2;3;168;211ma\u001b[39m"]
[54.831583, "o", "\u001b[38;2;1;180;201m3\u001b[39m\u001b[38;2;1;191;191m5\u001b[39m"]
[54.831645, "o", "\u001b[38;2;1;202;179me\u001b[39m"]
[54.831696, "o", "\u001b[38;2;3;212;167m0\u001b[39m\u001b[38;2;6;221;155md\u001b[39m"]
[54.831747, "o", "\u001b[38;2;11;229;143ma\u001b[39m"]
[54.831797, "o", "\u001b[38;2;16;236;130m6\u001b[39m"]
[54.831848, "o", "\u001b[38;2;23;242;117m2\u001b[39m\u001b[38;2;31;247;105m5\u001b[39m"]
[54.831898, "o", "\u001b[38;2;39;251;92m9\u001b[39m"]
[54.831948, "o", "\u001b[38;2;49;253;80m2\u001b[39m"]
[54.831999, "o", "\u001b[38;2;59;254;69md\u001b[39m\u001b[38;2;70;254;58me\u001b[39m"]
[54.832049, "o", "\u001b[38;2;82;253;48m5\u001b[39m"]
[54.8321, "o", "\u001b[38;2;94;250;38ma\u001b[39m"]
[54.832149, "o", "\u001b[38;2;106;246;30m0\u001b[39m"]
[54.8322, "o", "\u001b[38;2;119;241;22m7\u001b[39m\u001b[38;2;132;235;15m6\u001b[39m\u001b[38;2;144;228;10ma\u001b[39m"]
[54.832265, "o", "\u001b[38;2;157;220;6m5\u001b[39m"]
[54.832324, "o", "\u001b[38;2;169;211;3m8\u001b[39m"]
[54.832371, "o", "\u001b[38;2;181;201;1m1\u001b[39m"]
[54.832421, "o", "\u001b[38;2;192;190;1m1\u001b[39m\u001b[38;2;203;179;1m9\u001b[39m"]
[54.832469, "o", "\u001b[38;2;213;167;3m0\u001b[39m"]
[54.832518, "o", "\u001b[38;2;222;154;7m4\u001b[39m"]
[54.832565, "o", "\u001b[38;2;230;142;11m5\u001b[39m\u001b[38;2;237;129;17md\u001b[39m"]
[54.832614, "o", "\u001b[38;2;243;117;23m2\u001b[39m"]
[54.832665, "o", "\u001b[38;2;247;104;31m4\u001b[39m\u001b[38;2;251;92;40m9\u001b[39m"]
[54.832733, "o", "\u001b[38;2;253;80;50m0\u001b[39m"]
[54.832783, "o", "\u001b[38;2;254;68;60m8\u001b[39m\u001b[38;2;254;57;71m9\u001b[39m"]
[54.832831, "o", "\u001b[38;2;253;47;83m1\u001b[39m"]
[54.83288, "o", "\u001b[38;2;250;38;95m9\u001b[39m"]
[54.832927, "o", "\u001b[38;2;246;29;107m \u001b[39m\u001b[38;2;241;22;120m \u001b[39m"]
[54.832975, "o", "\u001b[38;2;235;15;132m.\u001b[39m"]
[54.833023, "o", "\u001b[38;2;228;10;145m/\u001b[39m"]
[54.833121, "o", "\u001b[38;2;219;6;158mi\u001b[39m"]
[54.83321, "o", "\u001b[38;2;210;3;170mc\u001b[39m"]
[54.833278, "o", "\u001b[38;2;200;1;182mo\u001b[39m\u001b[38;2;189;1;193mn\u001b[39m"]
[54.83331, "o", "\u001b[38;2;178;1;203m.\u001b[39m"]
[54.833363, "o", "\u001b[38;2;166;3;213mp\u001b[39m"]
[54.833433, "o", "\u001b[38;2;154;7;222mn\u001b[39m\u001b[38;2;141;11;230mg\u001b[39m\u001b[38;2;129;17;237m\u001b[39m"]
[54.833487, "o", "\r\n"]
[54.833545, "o", "\u001b[38;2;3;168;211md\u001b[39m"]
[54.833605, "o", "\u001b[38;2;1;180;201m8\u001b[39m"]
[54.833658, "o", "\u001b[38;2;1;191;191ma\u001b[39m"]
[54.833717, "o", "\u001b[38;2;1;202;179m1\u001b[39m\u001b[38;2;3;212;167m0\u001b[39m"]
[54.833786, "o", "\u001b[38;2;6;221;155me\u001b[39m"]
[54.833864, "o", "\u001b[38;2;11;229;143m8\u001b[39m"]
[54.833928, "o", "\u001b[38;2;16;236;130m5\u001b[39m\u001b[38;2;23;242;117m7\u001b[39m"]
[54.833993, "o", "\u001b[38;2;31;247;105m1\u001b[39m"]
[54.834054, "o", "\u001b[38;2;39;251;92mb\u001b[39m\u001b[38;2;49;253;80m7\u001b[39m"]
[54.834111, "o", "\u001b[38;2;59;254;69m8\u001b[39m\u001b[38;2;70;254;58m9\u001b[39m"]
[54.834177, "o", "\u001b[38;2;82;253;48mb\u001b[39m"]
[54.834256, "o", "\u001b[38;2;94;250;38mc\u001b[39m\u001b[38;2;106;246;30m7\u001b[39m"]
[54.834328, "o", "\u001b[38;2;119;241;22ma\u001b[39m\u001b[38;2;132;235;15mc"]
[54.834387, "o", "\u001b[39m"]
[54.834938, "o", "\u001b[38;2;144;228;10m0\u001b[39m"]
[54.83501, "o", "\u001b[38;2;157;220;6m9\u001b[39m\u001b[38;2;169;211;3mb\u001b[39m"]
[54.83508, "o", "\u001b[38;2;181;201;1ma\u001b[39m\u001b[38;2;192;190;1me\u001b[39m\u001b[38;2;203;179;1m"]
[54.835137, "o", "2\u001b[39m\u001b[38;2;213;167;3mc\u001b[39m"]
[54.835222, "o", "\u001b[38;2;222;154;7m8\u001b[39m\u001b[38;2;230;142;11m"]
[54.835296, "o", "e\u001b[39m\u001b[38;2;237;129;17me\u001b[39m\u001b[38;2;243;117;23m1\u001b[39m"]
[54.835365, "o", "\u001b[38;2;247;104;31m2\u001b[39m\u001b[38;2;251;92;40m"]
[54.835432, "o", "3\u001b[39m\u001b[38;2;253;80;50m6\u001b[39m"]
[54.835485, "o", "\u001b[38;2;254;68;60ma\u001b[39m\u001b[38;2;254;57;71m2\u001b[39m"]
[54.835546, "o", "\u001b[38;2;253;47;83mf\u001b[39m\u001b[38;2;250;38;95m"]
[54.835612, "o", "a\u001b[39m\u001b[38;2;246;29;107mc\u001b[39m"]
[54.835676, "o", "\u001b[38;2;241;22;120ma\u001b[39m\u001b[38;2;235;15;132ma\u001b[39m"]
[54.835731, "o", "\u001b[38;2;228;10;145m \u001b[39m\u001b[38;2;219;6;158m \u001b[39m"]
[54.835791, "o", "\u001b[38;2;210;3;170m.\u001b[39m"]
[54.835858, "o", "\u001b[38;2;200;1;182m/\u001b[39m\u001b[38;2;189;1;193mi\u001b[39m"]
[54.835922, "o", "\u001b[38;2;178;1;203mc\u001b[39m\u001b[38;2;166;3;213mo\u001b[39m"]
[54.835974, "o", "\u001b[38;2;154;7;222mn\u001b[39m"]
[54.83603, "o", "\u001b[38;2;141;11;230m.\u001b[39m\u001b[38;2;129;17;237mp\u001b[39m"]
[54.836085, "o", "\u001b[38;2;116;24;243mn\u001b[39m"]
[54.836148, "o", "\u001b[38;2;103;32;248mg\u001b[39m\u001b[38;2;91;40;251m.\u001b[39m"]
[54.836213, "o", "\u001b[38;2;79;50;253mi\u001b[39m\u001b[38;2;68;61;254mm\u001b[39m"]
[54.83628, "o", "\u001b[38;2;57;72;254mp\u001b[39m\u001b[38;2;46;83;253mo\u001b[39m"]
[54.836353, "o", "\u001b[38;2;37;95;250mr\u001b[39m\u001b[38;2;29;108;246mt"]
[54.836421, "o", "\u001b[39m\u001b[38;2;21;120;241m"]
[54.836493, "o", "\u001b[39m\r\n"]
[54.836592, "o", "\u001b[38;2;1;202;179m5\u001b[39m"]
[54.836652, "o", "\u001b[38;2;3;212;167m2\u001b[39m"]
[54.836709, "o", "\u001b[38;2;6;221;155m0\u001b[39m\u001b[38;2;11;229;143mc\u001b[39m"]
[54.83677, "o", "\u001b[38;2;16;236;130m7\u001b[39m"]
[54.836841, "o", "\u001b[38;2;23;242;117m0\u001b[39m\u001b[38;2;31;247;105m8\u001b[39m"]
[54.836898, "o", "\u001b[38;2;39;251;92mb\u001b[39m"]
[54.836965, "o", "\u001b[38;2;49;253;80me\u001b[39m\u001b[38;2;59;254;69ma\u001b[39m"]
[54.837021, "o", "\u001b[38;2;70;254;58m1\u001b[39m"]
[54.837075, "o", "\u001b[38;2;82;253;48m5\u001b[39m"]
[54.837132, "o", "\u001b[38;2;94;250;38ma\u001b[39m"]
[54.837194, "o", "\u001b[38;2;106;246;30m1\u001b[39m\u001b[38;2;119;241;22mf\u001b[39m"]
[54.837257, "o", "\u001b[38;2;132;235;15m5\u001b[39m"]
[54.837331, "o", "\u001b[38;2;144;228;10ma\u001b[39m\u001b[38;2;157;220;6m2\u001b[39m"]
[54.837398, "o", "\u001b[38;2;169;211;3m7\u001b[39m\u001b[38;2;181;201;1mc\u001b[39m"]
[54.837465, "o", "\u001b[38;2;192;190;1ma\u001b[39m\u001b[38;2;203;179;1m0\u001b[39m"]
[54.83753, "o", "\u001b[38;2;213;167;3mb\u001b[39m\u001b[38;2;222;154;7m9"]
[54.837594, "o", "\u001b[39m\u001b[38;2;230;142;11md\u001b[39m"]
[54.83765, "o", "\u001b[38;2;237;129;17mf\u001b[39m\u001b[38;2;243;117;23ma\u001b[39m"]
[54.83771, "o", "\u001b[38;2;247;104;31m8\u001b[39m"]
[54.837767, "o", "\u001b[38;2;251;92;40m8\u001b[39m\u001b[38;2;253;80;50mc\u001b[39m"]
[54.837824, "o", "\u001b[38;2;254;68;60m7\u001b[39m\u001b[38;2;254;57;71mf\u001b[39m"]
[54.837888, "o", "\u001b[38;2;253;47;83m0\u001b[39m\u001b[38;2;250;38;95ma\u001b[39m"]
[54.837966, "o", "\u001b[38;2;246;29;107mc\u001b[39m\u001b[38;2;241;22;120m"]
[54.838041, "o", "0\u001b[39m\u001b[38;2;235;15;132m4\u001b[39m"]
[54.83811, "o", "\u001b[38;2;228;10;145me\u001b[39m"]
[54.838176, "o", "\u001b[38;2;219;6;158mb\u001b[39m\u001b[38;2;210;3;170m6\u001b[39m"]
[54.838241, "o", "\u001b[38;2;200;1;182m \u001b[39m\u001b[38;2;189;1;193m \u001b[39m"]
[54.83832, "o", "\u001b[38;2;178;1;203m.\u001b[39m\u001b[38;2;166;3;213m/\u001b[39m"]
[54.838353, "o", "\u001b[38;2;154;7;222mL\u001b[39m\u001b[38;2;141;11;230mI"]
[54.838418, "o", "\u001b[39m\u001b[38;2;129;17;237mC\u001b[39m"]
[54.838464, "o", "\u001b[38;2;116;24;243mE\u001b[39m\u001b[38;2;103;32;248mN"]
[54.838512, "o", "\u001b[39m\u001b[38;2;91;40;251mS\u001b[39m"]
[54.838572, "o", "\u001b[38;2;79;50;253mE\u001b[39m\u001b[38;2;68;61;254m\u001b[39m\r\n"]
[54.838665, "o", "\u001b[38;2;11;229;143m2\u001b[39m"]
[54.838718, "o", "\u001b[38;2;16;236;130mb\u001b[39m"]
[54.838787, "o", "\u001b[38;2;23;242;117m9\u001b[39m\u001b[38;2;31;247;105me\u001b[39m"]
[54.838987, "o", "\u001b[38;2;39;251;92m7\u001b[39m"]
[54.839081, "o", "\u001b[38;2;49;253;80mf\u001b[39m"]
[54.839181, "o", "\u001b[38;2;59;254;69m6\u001b[39m"]
[54.839246, "o", "\u001b[38;2;70;254;58m0\u001b[39m\u001b[38;2;82;253;48m6\u001b[39m"]
[54.839312, "o", "\u001b[38;2;94;250;38mf\u001b[39m\u001b[38;2;106;246;30m9\u001b[39m"]
[54.839376, "o", "\u001b[38;2;119;241;22m5\u001b[39m"]
[54.839429, "o", "\u001b[38;2;132;235;15mc\u001b[39m\u001b[38;2;144;228;10m5\u001b[39m"]
[54.839482, "o", "\u001b[38;2;157;220;6me\u001b[39m\u001b[38;2;169;211;3m"]
[54.839535, "o", "c\u001b[39m\u001b[38;2;181;201;1m6\u001b[39m"]
[54.839587, "o", "\u001b[38;2;192;190;1m9\u001b[39m"]
[54.839637, "o", "\u001b[38;2;203;179;1md\u001b[39m\u001b[38;2;213;167;3m7\u001b[39m"]
[54.839688, "o", "\u001b[38;2;222;154;7m9\u001b[39m"]
[54.839738, "o", "\u001b[38;2;230;142;11mc\u001b[39m"]
[54.839787, "o", "\u001b[38;2;237;129;17md\u001b[39m\u001b[38;2;243;117;23mc\u001b[39m"]
[54.839842, "o", "\u001b[38;2;247;104;31m4\u001b[39m"]
[54.839885, "o", "\u001b[38;2;251;92;40m1\u001b[39m"]
[54.839941, "o", "\u001b[38;2;253;80;50m3\u001b[39m\u001b[38;2;254;68;60mb\u001b[39m"]
[54.83999, "o", "\u001b[38;2;254;57;71m0\u001b[39m"]
[54.840043, "o", "\u001b[38;2;253;47;83mb\u001b[39m\u001b[38;2;250;38;95m0\u001b[39m"]
[54.840089, "o", "\u001b[38;2;246;29;107md\u001b[39m"]
[54.840151, "o", "\u001b[38;2;241;22;120m9\u001b[39m\u001b[38;2;235;15;132mf"]
[54.840196, "o", "\u001b[39m\u001b[38;2;228;10;145ma\u001b[39m"]
[54.840245, "o", "\u001b[38;2;219;6;158m0\u001b[39m"]
[54.840306, "o", "\u001b[38;2;210;3;170me\u001b[39m"]
[54.840357, "o", "\u001b[38;2;200;1;182m8\u001b[39m\u001b[38;2;189;1;193m8\u001b[39m"]
[54.840409, "o", "\u001b[38;2;178;1;203ma\u001b[39m"]
[54.840458, "o", "\u001b[38;2;166;3;213m \u001b[39m\u001b[38;2;154;7;222m \u001b[39m"]
[54.840507, "o", "\u001b[38;2;141;11;230m.\u001b[39m"]
[54.840558, "o", "\u001b[38;2;129;17;237m/\u001b[39m"]
[54.840609, "o", "\u001b[38;2;116;24;243mn\u001b[39m\u001b[38;2;103;32;248me\u001b[39m"]
[54.840662, "o", "\u001b[38;2;91;40;251mw\u001b[39m"]
[54.840709, "o", "\u001b[38;2;79;50;253m.\u001b[39m\u001b[38;2;68;61;254m"]
[54.840762, "o", "c\u001b[39m"]
[54.841327, "o", "\u001b[38;2;57;72;254ma\u001b[39m"]
[54.841424, "o", "\u001b[38;2;46;83;253ms\u001b[39m\u001b[38;2;37;95;250mt\u001b[39m\u001b[38;2;29;108;246m\u001b[39m\r\n"]
[54.841493, "o", "\u001b[38;2;31;247;105mb\u001b[39m\u001b[38;2;39;251;92mb\u001b[39m"]
[54.841552, "o", "\u001b[38;2;49;253;80m9\u001b[39m"]
[54.841607, "o", "\u001b[38;2;59;254;69me\u001b[39m\u001b[38;2;70;254;58m2\u001b[39m"]
[54.841663, "o", "\u001b[38;2;82;253;48ma\u001b[39m"]
[54.841719, "o", "\u001b[38;2;94;250;38ma\u001b[39m\u001b[38;2;106;246;30m7\u001b[39m"]
[54.841779, "o", "\u001b[38;2;119;241;22m9\u001b[39m"]
[54.841812, "o", "\u001b[38;2;132;235;15ma\u001b[39m\u001b[38;2;144;228;10m6\u001b[39m"]
[54.841885, "o", "\u001b[38;2;157;220;6ma\u001b[39m"]
[54.841945, "o", "\u001b[38;2;169;211;3m6\u001b[39m\u001b[38;2;181;201;1mc\u001b[39m"]
[54.842006, "o", "\u001b[38;2;192;190;1m9\u001b[39m"]
[54.842066, "o", "\u001b[38;2;203;179;1m4\u001b[39m\u001b[38;2;213;167;3m"]
[54.842122, "o", "d\u001b[39m\u001b[38;2;222;154;7m4\u001b[39m"]
[54.842176, "o", "\u001b[38;2;230;142;11m4\u001b[39m"]
[54.842263, "o", "\u001b[38;2;237;129;17m6\u001b[39m\u001b[38;2;243;117;23m0\u001b[39m"]
[54.842396, "o", "\u001b[38;2;247;104;31m8\u001b[39m"]
[54.842476, "o", "\u001b[38;2;251;92;40m4\u001b[39m"]
[54.842562, "o", "\u001b[38;2;253;80;50m1\u001b[39m\u001b[38;2;254;68;60me\u001b[39m"]
[54.842659, "o", "\u001b[38;2;254;57;71m0\u001b[39m\u001b[38;2;253;47;83m5\u001b[39m"]
[54.842742, "o", "\u001b[38;2;250;38;95m3\u001b[39m\u001b[38;2;246;29;107m9\u001b[39m"]
[54.842848, "o", "\u001b[38;2;241;22;120m4\u001b[39m"]
[54.842933, "o", "\u001b[38;2;235;15;132m8\u001b[39m\u001b[38;2;228;10;145m4\u001b[39m\u001b[38;2;219;6;158me\u001b[39m"]
[54.843016, "o", "\u001b[38;2;210;3;170md\u001b[39m\u001b[38;2;200;1;182mc\u001b[39m\u001b[38;2;189;1;193m0\u001b[39m"]
[54.843101, "o", "\u001b[38;2;178;1;203m5\u001b[39m\u001b[38;2;166;3;213mf\u001b[39m\u001b[38;2;154;7;222ma\u001b[39m"]
[54.843189, "o", "\u001b[38;2;141;11;230m5\u001b[39m\u001b[38;2;129;17;237m \u001b[39m\u001b[38;2;116;24;243m \u001b[39m"]
[54.843298, "o", "\u001b[38;2;103;32;248m.\u001b[39m\u001b[38;2;91;40;251m/\u001b[39m"]
[54.843338, "o", "\u001b[38;2;79;50;253mp\u001b[39m\u001b[38;2;68;61;254mr\u001b[39m"]
[54.843438, "o", "\u001b[38;2;57;72;254mo\u001b[39m\u001b[38;2;46;83;253mj\u001b[39m\u001b[38;2;37;95;250me\u001b[39m"]
[54.843511, "o", "\u001b[38;2;29;108;246mc\u001b[39m\u001b[38;2;21;120;241mt\u001b[39m\u001b[38;2;15;133;235m.\u001b[39m"]
[54.843584, "o", "\u001b[38;2;10;146;227mg\u001b[39m\u001b[38;2;5;158;219mo\u001b[39m\u001b[38;2;3;170;210md"]
[54.843652, "o", "\u001b[39m\u001b[38;2;1;182;199mo\u001b[39m\u001b[38;2;1;193;189mt\u001b[39m"]
[54.843723, "o", "\u001b[38;2;1;204;177m\u001b[39m\r\n"]
[54.843797, "o", "\u001b[38;2;59;254;69m3\u001b[39m"]
[54.84387, "o", "\u001b[38;2;70;254;58ma\u001b[39m\u001b[38;2;82;253;48m8\u001b[39m\u001b[38;2;94;250;38m3"]
[54.843944, "o", "\u001b[39m\u001b[38;2;106;246;30m0\u001b[39m\u001b[38;2;119;241;22m0\u001b[39m"]
[54.844009, "o", "\u001b[38;2;132;235;15m5\u001b[39m\u001b[38;2;144;228;10ma\u001b[39m"]
[54.844071, "o", "\u001b[38;2;157;220;6m5\u001b[39m\u001b[38;2;169;211;3md\u001b[39m"]
[54.844155, "o", "\u001b[38;2;181;201;1mc\u001b[39m\u001b[38;2;192;190;1m8\u001b[39m"]
[54.844245, "o", "\u001b[38;2;203;179;1mb\u001b[39m\u001b[38;2;213;167;3ma\u001b[39m\u001b[38;2;222;154;7m5"]
[54.844341, "o", "\u001b[39m\u001b[38;2;230;142;11md\u001b[39m\u001b[38;2;237;129;17m0\u001b[39m\u001b[38;2;243;117;23mf\u001b[39m\u001b[38;2;247;104;31m0\u001b[39m"]
[54.844424, "o", "\u001b[38;2;251;92;40ma\u001b[39m\u001b[38;2;253;80;50mb\u001b[39m"]
[54.844495, "o", "\u001b[38;2;254;68;60mb\u001b[39m\u001b[38;2;254;57;71m3\u001b[39m"]
[54.844563, "o", "\u001b[38;2;253;47;83md\u001b[39m\u001b[38;2;250;38;95me\u001b[39m"]
[54.844639, "o", "\u001b[38;2;246;29;107m8\u001b[39m\u001b[38;2;241;22;120m4\u001b[39m\u001b[38;2;235;15;132m7\u001b[39m\u001b[38;2;228;10;145md\u001b[39m"]
[54.844728, "o", "\u001b[38;2;219;6;158m2\u001b[39m\u001b[38;2;210;3;170md\u001b[39m"]
[54.844818, "o", "\u001b[38;2;200;1;182me\u001b[39m\u001b[38;2;189;1;193m0\u001b[39m\u001b[38;2;178;1;203ma\u001b[39m"]
[54.844893, "o", "\u001b[38;2;166;3;213m4\u001b[39m\u001b[38;2;154;7;222m6\u001b[39m\u001b[38;2;141;11;230md\u001b[39m\u001b[38;2;129;17;237m7\u001b[39m"]
[54.84499, "o", "\u001b[38;2;116;24;243m7\u001b[39m\u001b[38;2;103;32;248mb\u001b[39m\u001b[38;2;91;40;251m \u001b[39m"]
[54.845083, "o", "\u001b[38;2;79;50;253m \u001b[39m\u001b[38;2;68;61;254m.\u001b[39m\u001b[38;2;57;72;254m/\u001b[39m\u001b[38;2;46;83;253mR\u001b[39m\u001b[38;2;37;95;250mE\u001b[39m"]
[54.845178, "o", "\u001b[38;2;29;108;246mA\u001b[39m\u001b[38;2;21;120;241mD\u001b[39m"]
[54.845256, "o", "\u001b[38;2;15;133;235mM\u001b[39m\u001b[38;2;10;146;227mE\u001b[39m"]
[54.845343, "o", "\u001b[38;2;5;158;219m.\u001b[39m\u001b[38;2;3;170;210mm\u001b[39m\u001b[38;2;1;182;199md\u001b[39m"]
[54.845422, "o", "\u001b[38;2;1;193;189m\u001b[39m\r\n"]
[54.845491, "o", "\u001b[m\u001b[?25h\u001b[?1;5;2004l"]
[54.847349, "o", "\u001b]2;laptop:leroy:~/projects/godot-xterm\u0007\r\r\n\u001b[1;32m[\u001b]0;leroy@laptop: ~/projects/godot-xterm\u0007leroy@laptop:~/projects/godot-xterm]$\u001b[0m "]
[57.064686, "o", "e"]
[57.230802, "o", "x"]
[59.010363, "o", "\b\u001b[K"]
[59.121216, "o", "\b\u001b[K"]
[59.456844, "o", "#"]
[59.676225, "o", "/"]
[60.10367, "o", "\b\u001b[K"]
[60.18748, "o", " "]
[60.4099, "o", "w"]
[60.748895, "o", "e"]
[60.992399, "o", "\b\u001b[K"]
[61.116881, "o", "\b\u001b[K"]
[61.313248, "o", "W"]
[61.473147, "o", "h"]
[61.562029, "o", "e"]
[61.645136, "o", "n"]
[61.917431, "o", " "]
[62.093406, "o", "w"]
[62.161239, "o", "e"]
[62.248416, "o", " "]
[62.33829, "o", "a"]
[62.415246, "o", "r"]
[62.507794, "o", "e"]
[62.557211, "o", " "]
[62.753875, "o", "f"]
[62.85347, "o", "i"]
[62.921221, "o", "n"]
[63.033374, "o", "i"]
[63.094372, "o", "s"]
[63.187799, "o", "h"]
[63.483077, "o", "e"]
[63.612519, "o", "d"]
[63.792416, "o", ","]
[63.894738, "o", " "]
[64.04963, "o", "j"]
[64.202261, "o", "u"]
[64.263783, "o", "s"]
[64.307216, "o", "t"]
[64.369715, "o", " "]
[64.492205, "o", "e"]
[64.653815, "o", "x"]
[64.746695, "o", "i"]
[64.858507, "o", "t"]
[65.103114, "o", " "]
[65.664488, "o", "t"]
[65.719435, "o", "h"]
[65.782418, "o", "e"]
[65.844266, "o", " "]
[65.963151, "o", "s"]
[66.037719, "o", "h"]
[66.121665, "o", "e"]
[66.224533, "o", "l"]
[66.323957, "o", "l"]
[66.562302, "o", "\r\n"]
[66.562831, "o", "\u001b]2;laptop:leroy:~/projects/godot-xterm\u0007\r\r\n\u001b[1;32m[\u001b]0;leroy@laptop: ~/projects/godot-xterm\u0007leroy@laptop:~/projects/godot-xterm]$\u001b[0m "]
[66.844263, "o", "e"]
[66.983977, "o", "x"]
[67.104686, "o", "i"]
[67.212146, "o", "t"]
[67.515156, "o", "\r\n"]
[67.515263, "o", "exit\r\n"]
[67.516389, "o", "\u001b[0;32masciinema: recording finished\u001b[0m\r\n\u001b[0;32masciinema: asciicast saved to new.cast\u001b[0m\r\n"]
[67.526166, "o", "sh-4.4$ "]
[69.107982, "o", "c"]
[69.201795, "o", "o"]
[69.306066, "o", "w"]
[69.520215, "o", "s"]
[69.638453, "o", "a"]
[69.696601, "o", "y"]
[70.376247, "o", " "]
[70.997599, "o", "A"]
[71.148845, "o", "l"]
[71.283482, "o", "l"]
[71.322607, "o", " "]
[71.823544, "o", "d"]
[71.910819, "o", "o"]
[72.005256, "o", "n"]
[72.059912, "o", "e"]
[72.234089, "o", "!"]
[72.62946, "o", "\r\n"]
[72.64376, "o", " ___________ \r\n< All done! >\r\n ----------- \r\n \\ ^__^\r\n \\ (oo)\\_______\r\n (__)\\ )\\/\\\r\n ||----w |\r\n || ||\r\n"]
[72.644144, "o", "sh-4.4$ "]
[73.525287, "o", "\r\n"]
[73.525434, "o", "sh-4.4$ "]
[74.180823, "o", "e"]
[74.345967, "o", "x"]
[74.476496, "o", "i"]
[74.626928, "o", "t"]
[75.019672, "o", "\r\nexit\r\n"]

149
examples/menu/menu.gd Normal file
View file

@ -0,0 +1,149 @@
tool
extends Control
# This scene demonstrates how we can control the Terminal node directly by
# sending and receiving strings and ANSI escape sequences to the terminal
# directly.
# References:
# - https://tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html
# - https://www.youtube.com/watch?v=jTSQlIK_92w
const TITLE = """
\r
\r
\r
"""
const TITLE_WIDTH = 42
var menu_items := [
{ "name": "Asciicast", "scene": preload("../asciicast/asciicast.tscn") },
{ "name": "Terminal", "scene": preload("../terminal/terminal.tscn") },
{ "name": "Exit"}
]
var selected_index := 0
var row: int
var menu_start_row: int
var offset: int
onready var tput = TPut.new($Terminal)
func _ready():
# warning-ignore:return_value_discarded
$Terminal.connect("size_changed", self, "draw_all")
$Terminal.grab_focus()
draw_all()
func draw_all(_size = Vector2.ZERO):
offset = int(floor(($Terminal.cols / 2.0) - (TITLE_WIDTH / 2.0)))
tput.reset()
row = 5
tput.civis() # Hide the cursor.
draw_title()
draw_menu()
tput.sgr0()
row += 1
func draw_title():
tput.cup(row, 0)
for line in TITLE.split("\r"):
row += 1
tput.cup(row, offset)
$Terminal.write(line)
# Get the plugin version from the plugin's config file.
var config = ConfigFile.new()
var err = config.load("res://addons/godot_xterm/plugin.cfg")
if err == OK:
$Terminal.write("\n")
$Terminal.write("Version: %s" % config.get_value("plugin", "version",
"unknown"))
row += 2
func draw_menu():
if not menu_start_row:
menu_start_row = row + 1
row = menu_start_row
var col_offset: int
for i in range(menu_items.size()):
row += 1
var item = menu_items[i]
if not col_offset:
col_offset = int(floor(($Terminal.cols / 2) - (item.name.length() / 2)))
tput.cup(row, offset)
if selected_index == i:
tput.setab(Color("#FF7500"))
tput.setaf(Color.black)
$Terminal.write("%s. %s" % [i + 1, item.name])
if selected_index == i:
tput.sgr0()
func _on_Terminal_key_pressed(data: String, event: InputEventKey) -> void:
match(data):
TPut.CURSOR_UP: # Up arrow key
selected_index = int(clamp(selected_index - 1, 0, menu_items.size() - 1))
draw_menu()
TPut.CURSOR_DOWN: # Down arrow key
selected_index = int(clamp(selected_index + 1, 0, menu_items.size() - 1))
draw_menu()
"1":
selected_index = 0
draw_menu()
"2":
selected_index = 1
draw_menu()
"3":
selected_index = 2
draw_menu()
# We can also match against the raw InputEventKey.
if event.scancode == KEY_ENTER:
var item = menu_items[selected_index]
match item.name:
"Asciicast":
var scene = item.scene.instance()
var animation_player: AnimationPlayer = scene.get_node("AnimationPlayer")
scene.connect("key_pressed", self, "_on_Asciicast_key_pressed",
[animation_player])
get_tree().get_root().add_child(scene)
visible = false
scene.grab_focus()
yield(animation_player, "animation_finished")
visible = true
get_tree().get_root().remove_child(scene)
$Terminal.grab_focus()
scene.queue_free()
"Terminal":
var scene = item.scene.instance()
var pty = scene.get_node("Pseudoterminal")
get_tree().get_root().add_child(scene)
visible = false
scene.grab_focus()
yield(pty, "exited")
visible = true
$Terminal.grab_focus()
scene.queue_free()
"Exit":
get_tree().quit()
func _on_Asciicast_key_pressed(data: String, _event: InputEventKey,
animation_player: AnimationPlayer) -> void:
if data == "\u001b":
animation_player.emit_signal("animation_finished")

28
examples/menu/menu.tscn Normal file
View file

@ -0,0 +1,28 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://addons/godot_xterm/nodes/terminal/terminal.gdns" type="Script" id=1]
[ext_resource path="res://examples/menu/menu.gd" type="Script" id=2]
[ext_resource path="res://addons/godot_xterm/themes/default.theme" type="Theme" id=3]
[node name="Menu" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 2 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Terminal" type="Control" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
focus_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme = ExtResource( 3 )
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
rows = 31
cols = 102
[connection signal="key_pressed" from="Terminal" to="." method="_on_Terminal_key_pressed"]

View file

@ -0,0 +1,7 @@
extends Node
func _on_Terminal_key_pressed(event: InputEventKey, data: PoolByteArray):
print(data as Array)
print(event.scancode)

View file

@ -8,15 +8,16 @@
anchor_right = 1.0
anchor_bottom = 1.0
focus_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme = ExtResource( 4 )
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
rows = 31
cols = 102
[node name="Pseudoterminal" type="Node" parent="."]
script = ExtResource( 2 )
[connection signal="data_read" from="." to="Pseudoterminal" method="put_data"]
[connection signal="data_received" from="Pseudoterminal" to="." method="write"]
[connection signal="data_sent" from="." to="Pseudoterminal" method="write"]
[connection signal="size_changed" from="." to="Pseudoterminal" method="resize"]
[connection signal="data_sent" from="Pseudoterminal" to="." method="write"]

View file

@ -10,23 +10,30 @@ config_version=4
_global_script_classes=[ {
"base": "Reference",
"class": "ArrayUtils",
"class": "TPut",
"language": "GDScript",
"path": "res://addons/SIsilicon.3d.text/array_utils.gd"
"path": "res://addons/godot_xterm/util/tput.gd"
} ]
_global_script_class_icons={
"ArrayUtils": ""
"TPut": ""
}
[application]
config/name="Godot Xterm"
run/main_scene="res://examples/menu/menu.tscn"
config/icon="res://icon.png"
[display]
window/vsync/use_vsync=false
[editor_plugins]
enabled=PoolStringArray( "SIsilicon.3d.text", "godot_xterm" )
enabled=PoolStringArray( "godot_xterm" )
[rendering]
quality/filters/anisotropic_filter_level=16
quality/filters/msaa=4
environment/default_environment="res://default_env.tres"