Add asciicast importer

This commit is contained in:
Leroy Hopson 2020-09-22 15:31:46 +07:00
parent 55544de93e
commit ffa8561865
11 changed files with 199 additions and 77 deletions

View file

@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- Changelog. - Changelog.
- Asciicast importer plugin. Enables the import of .cast ([asciicast files v2](https://github.com/asciinema/asciinema/blob/master/doc/asciicast-v2.md)) that can be made using the [asciinema](https://asciinema.org/) terminal session recorder. See the [asciicast scene](/examples/asciicast) for example usage.
### Changed ### Changed
- Implementation of Terminal node from GDScript to GDNative using [Aetf's patched version of libtsm](https://github.com/Aetf/libtsm). - Implementation of Terminal node from GDScript to GDNative using [Aetf's patched version of libtsm](https://github.com/Aetf/libtsm).

View file

@ -18,6 +18,13 @@ Click the thumbnail to watch a demo video on youtube:
[![Demo video thumbnail](https://img.youtube.com/vi/_Tt4eQEBybo/0.jpg)](https://www.youtube.com/watch?v=_Tt4eQEBybo) [![Demo video thumbnail](https://img.youtube.com/vi/_Tt4eQEBybo/0.jpg)](https://www.youtube.com/watch?v=_Tt4eQEBybo)
## Usage
### Asciicast (.cast) file importer
[Asciinema](https://asciinema.org) recordings saved with the `.cast` extension will be automatically imported as animations. They can then be added to AnimationPlayer which is a child of the Terminal node. Playing the animation will play the terminal session recording in the parent Terminal.
For an example, see the scene in [/examples/asciicast](/examples/asciicast).
## License ## License
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license.

View file

@ -0,0 +1,69 @@
tool
extends EditorImportPlugin
const Asciicast = preload("res://addons/godot_xterm/resources/asciicast.gd")
func get_importer_name():
return "godot_xterm"
func get_visible_name():
return "asciicast"
func get_recognized_extensions():
return ["cast"]
func get_save_extension():
return "res"
func get_resource_type():
return "Animation"
func get_import_options(preset):
return []
func get_preset_count():
return 0
func import(source_file, save_path, options, r_platform_variant, r_gen_files):
var file = File.new()
var err = file.open(source_file, File.READ)
if err != OK:
return err
var header = file.get_line()
var asciicast = Asciicast.new()
asciicast.add_track(Animation.TYPE_METHOD, 0)
asciicast.track_set_path(0, ".")
var time: float
while not file.eof_reached():
var line = file.get_line()
if line == "":
continue
var p = JSON.parse(line)
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.length = time
return ResourceSaver.save("%s.%s" % [save_path, get_save_extension()], asciicast)

View file

@ -1,16 +0,0 @@
extends Node
# Declare member variables here. Examples:
# var a = 2
# var b = "text"
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
#func _process(delta):
# pass

View file

@ -2,7 +2,16 @@ tool
extends EditorPlugin extends EditorPlugin
var asciicast_import_plugin
func _enter_tree(): func _enter_tree():
asciicast_import_plugin = preload("res://addons/godot_xterm/import_plugins/asciicast_import_plugin.gd").new()
add_import_plugin(asciicast_import_plugin)
var asciicast_script = preload("res://addons/godot_xterm/resources/asciicast.gd")
add_custom_type("Asciicast", "Animation", asciicast_script, null)
var terminal_script = preload("res://addons/godot_xterm/nodes/terminal/terminal.gdns") var terminal_script = preload("res://addons/godot_xterm/nodes/terminal/terminal.gdns")
var terminal_icon = preload("res://addons/godot_xterm/nodes/terminal/terminal_icon.svg") var terminal_icon = preload("res://addons/godot_xterm/nodes/terminal/terminal_icon.svg")
add_custom_type("Terminal", "Control", terminal_script, terminal_icon) add_custom_type("Terminal", "Control", terminal_script, terminal_icon)
@ -13,5 +22,9 @@ func _enter_tree():
func _exit_tree(): func _exit_tree():
remove_import_plugin(asciicast_import_plugin)
asciicast_import_plugin = null
remove_custom_type("Asciicast")
remove_custom_type("Terminal") remove_custom_type("Terminal")
remove_custom_type("Psuedoterminal") remove_custom_type("Psuedoterminal")

View file

@ -0,0 +1,19 @@
extends Animation
signal data_written(data)
signal data_read(data)
export(int) var version: int = 2
# Initial terminal width (number of columns).
export(int) var width: int
# Initial terminal height (number of rows).
export(int) var height: int
func get_class() -> String:
return "Asciicast"
func is_class(name) -> bool:
return name == get_class() or .is_class(name)

View file

@ -0,0 +1,53 @@
extends Container
# This Container ensures that the terminal always fills
# the window and/or screen. It also connects the terminal
# to the input/output of the Psuedoterminal.
const ESCAPE = 27
const BACKSPACE = 8
const BEEP = 7
const SPACE = 32
const LEFT_BRACKET = 91
const ENTER = 10
const BACKSPACE_ALT = 127
onready var viewport = get_viewport()
func _ready():
viewport.connect("size_changed", self, "_resize")
_resize()
$Terminal/AnimationPlayer.play("a")
func _input(event):
#return
if event is InputEventKey and event.pressed:
var data = PoolByteArray([])
accept_event()
# TODO: Handle more of these.
if (event.control and event.scancode == KEY_C):
data.append(3)
elif event.unicode:
data.append(event.unicode)
elif event.scancode == KEY_ENTER:
data.append(ENTER)
elif event.scancode == KEY_BACKSPACE:
data.append(BACKSPACE_ALT)
elif event.scancode == KEY_ESCAPE:
data.append(27)
elif event.scancode == KEY_TAB:
data.append(9)
elif OS.get_scancode_string(event.scancode) == "Shift":
pass
elif OS.get_scancode_string(event.scancode) == "Control":
pass
else:
pass
#push_warning('Unhandled input. scancode: ' + str(OS.get_scancode_string(event.scancode)))
#emit_signal("output", data)
func _resize():
rect_size = viewport.size
$Terminal.rect_size = rect_size

View file

@ -0,0 +1,24 @@
[gd_scene load_steps=5 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 )
__meta__ = {
"_edit_use_anchors_": false
}
[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"]
method_call_mode = 1
anims/a = ExtResource( 6 )

View file

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

View file

@ -0,0 +1,13 @@
[remap]
importer="godot_xterm"
type="Animation"
path="res://.import/example.cast-9299cc6d12357f676344c3d48e3179e0.res"
[deps]
source_file="res://examples/asciicast/example.cast"
dest_files=[ "res://.import/example.cast-9299cc6d12357f676344c3d48e3179e0.res" ]
[params]