diff --git a/CHANGELOG.md b/CHANGELOG.md index 7347e40..d0cec55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - 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 - Implementation of Terminal node from GDScript to GDNative using [Aetf's patched version of libtsm](https://github.com/Aetf/libtsm). diff --git a/README.md b/README.md index 3b73c53..c2577df 100644 --- a/README.md +++ b/README.md @@ -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) +## 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 If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. diff --git a/addons/godot_xterm/import_plugins/asciicast_import_plugin.gd b/addons/godot_xterm/import_plugins/asciicast_import_plugin.gd new file mode 100644 index 0000000..4830c1c --- /dev/null +++ b/addons/godot_xterm/import_plugins/asciicast_import_plugin.gd @@ -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) diff --git a/addons/godot_xterm/import_plugins/cast_import_plugin.gd b/addons/godot_xterm/import_plugins/cast_import_plugin.gd deleted file mode 100644 index 1eccaec..0000000 --- a/addons/godot_xterm/import_plugins/cast_import_plugin.gd +++ /dev/null @@ -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 diff --git a/addons/godot_xterm/plugin.gd b/addons/godot_xterm/plugin.gd index 57a118b..6824502 100644 --- a/addons/godot_xterm/plugin.gd +++ b/addons/godot_xterm/plugin.gd @@ -2,7 +2,16 @@ tool extends EditorPlugin +var asciicast_import_plugin + + 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_icon = preload("res://addons/godot_xterm/nodes/terminal/terminal_icon.svg") add_custom_type("Terminal", "Control", terminal_script, terminal_icon) @@ -13,5 +22,9 @@ func _enter_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("Psuedoterminal") diff --git a/addons/godot_xterm/resources/asciicast.gd b/addons/godot_xterm/resources/asciicast.gd new file mode 100644 index 0000000..02948b2 --- /dev/null +++ b/addons/godot_xterm/resources/asciicast.gd @@ -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) diff --git a/examples/asciicast/asciicast.gd b/examples/asciicast/asciicast.gd new file mode 100644 index 0000000..9ded643 --- /dev/null +++ b/examples/asciicast/asciicast.gd @@ -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 diff --git a/examples/asciicast/asciicast.tscn b/examples/asciicast/asciicast.tscn new file mode 100644 index 0000000..f655220 --- /dev/null +++ b/examples/asciicast/asciicast.tscn @@ -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 ) diff --git a/examples/asciicast/demo.cast b/examples/asciicast/demo.cast deleted file mode 100644 index afba5c6..0000000 --- a/examples/asciicast/demo.cast +++ /dev/null @@ -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"] diff --git a/demo.cast b/examples/asciicast/example.cast similarity index 100% rename from demo.cast rename to examples/asciicast/example.cast diff --git a/examples/asciicast/example.cast.import b/examples/asciicast/example.cast.import new file mode 100644 index 0000000..3ccf24a --- /dev/null +++ b/examples/asciicast/example.cast.import @@ -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] +