mirror of
https://github.com/lihop/godot-xterm.git
synced 2024-11-24 10:20:24 +01:00
WIP: Add readline util
This commit is contained in:
parent
04e07ec9a7
commit
52b7ac2970
6 changed files with 334 additions and 2 deletions
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
|
@ -214,7 +214,7 @@ jobs:
|
||||||
retention-days: 1 # Minimum.
|
retention-days: 1 # Minimum.
|
||||||
path: |
|
path: |
|
||||||
addons
|
addons
|
||||||
!addons/godot-xterm
|
!addons/godot_xterm
|
||||||
|
|
||||||
|
|
||||||
html5_export:
|
html5_export:
|
||||||
|
@ -286,7 +286,7 @@ jobs:
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: plugins
|
name: plugins
|
||||||
path: ./
|
path: ./addons
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
if: ${{ matrix.godot_version != 'v3.2-stable' }}
|
if: ${{ matrix.godot_version != 'v3.2-stable' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
[Unreleased]: https://github.com/lihop/godot-xterm/compare/v2.0.0...HEAD
|
[Unreleased]: https://github.com/lihop/godot-xterm/compare/v2.0.0...HEAD
|
||||||
|
### Added
|
||||||
|
- Basic readline utility that provides line editing capabilities similar to GNU Readline.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- TerminalSettings resource. Not currently used, and was showing up in every 'new resource' menu that is displayed after clicking on an empty Resource slot.
|
- TerminalSettings resource. Not currently used, and was showing up in every 'new resource' menu that is displayed after clicking on an empty Resource slot.
|
||||||
|
|
||||||
|
|
108
addons/godot_xterm/util/readline.gd
Normal file
108
addons/godot_xterm/util/readline.gd
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
# Copyright (c) 2021, Leroy Hopson (MIT License).
|
||||||
|
tool
|
||||||
|
extends Reference
|
||||||
|
|
||||||
|
const KeyCodes = {
|
||||||
|
ESCAPE = "\u001b",
|
||||||
|
ENTER = "\r",
|
||||||
|
BACKSPACE = "\u0008",
|
||||||
|
UP_ARROW = "\u001b[A",
|
||||||
|
DOWN_ARROW = "\u001b[B",
|
||||||
|
LEFT_ARROW = "\u001b[D",
|
||||||
|
RIGHT_ARROW = "\u001b[C",
|
||||||
|
}
|
||||||
|
|
||||||
|
var terminal
|
||||||
|
var _prompt: String
|
||||||
|
var _cursor_pos_min: int
|
||||||
|
var _cursor_pos: int
|
||||||
|
var _line: String
|
||||||
|
|
||||||
|
|
||||||
|
func _init(p_terminal) -> void:
|
||||||
|
terminal = p_terminal
|
||||||
|
assert(terminal.has_method("write"))
|
||||||
|
assert(terminal.has_signal("key_pressed"))
|
||||||
|
assert("cols" in terminal)
|
||||||
|
assert("rows" in terminal)
|
||||||
|
|
||||||
|
|
||||||
|
func readline(prompt := "> ") -> String:
|
||||||
|
_prompt = prompt
|
||||||
|
_cursor_pos_min = prompt.length()
|
||||||
|
_cursor_pos = prompt.length()
|
||||||
|
|
||||||
|
terminal.write(_prompt)
|
||||||
|
|
||||||
|
var input = yield(terminal, "key_pressed")
|
||||||
|
while input[1].scancode != KEY_ENTER:
|
||||||
|
print(input[0])
|
||||||
|
match input[0]:
|
||||||
|
KeyCodes.BACKSPACE:
|
||||||
|
_backspace()
|
||||||
|
KeyCodes.UP_ARROW:
|
||||||
|
# TODO: History prev.
|
||||||
|
pass
|
||||||
|
KeyCodes.DOWN_ARROW:
|
||||||
|
# TODO: History next.
|
||||||
|
pass
|
||||||
|
KeyCodes.LEFT_ARROW, KeyCodes.RIGHT_ARROW:
|
||||||
|
# TODO: Implement Me!
|
||||||
|
pass
|
||||||
|
_:
|
||||||
|
terminal.write(input[0])
|
||||||
|
_line += input[0]
|
||||||
|
_cursor_pos += 1
|
||||||
|
if _cursor_pos > 0 and _cursor_pos % int(terminal.cols) == 0:
|
||||||
|
terminal.write("\u001bE")
|
||||||
|
input = yield(terminal, "key_pressed")
|
||||||
|
|
||||||
|
return _line
|
||||||
|
|
||||||
|
|
||||||
|
func _backspace() -> void:
|
||||||
|
if _cursor_pos > _cursor_pos_min:
|
||||||
|
if _cursor_pos % int(terminal.cols) == 0:
|
||||||
|
terminal.write("\u001b[1A\u001b[%dC\u001b[K" % terminal.cols)
|
||||||
|
else:
|
||||||
|
terminal.write("\b \b")
|
||||||
|
_line = _line.substr(0, _cursor_pos - _cursor_pos_min - 1)
|
||||||
|
_cursor_pos -= 1
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_line() -> void:
|
||||||
|
var num_rows := ceil(_cursor_pos / terminal.cols)
|
||||||
|
for _row in range(num_rows):
|
||||||
|
terminal.write("\r\u001b[2K\u001b[1A")
|
||||||
|
terminal.write("\r\u001b[1B%s%s" % [_prompt, _line])
|
||||||
|
_cursor_pos = _prompt.length()
|
||||||
|
_cursor_pos += _line.length()
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
#func _add_history(line: String) -> void:
|
||||||
|
# _history.append(line)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
## TODO
|
||||||
|
#func _load_history(filepath) -> int:
|
||||||
|
# var file := File.new()
|
||||||
|
# var err := file.open(filepath, File.READ)
|
||||||
|
# if err == OK:
|
||||||
|
# var line := file.get_line()
|
||||||
|
# while line != "":
|
||||||
|
# _history.append(line)
|
||||||
|
# line = file.get_line()
|
||||||
|
# file.close()
|
||||||
|
# return err
|
||||||
|
#
|
||||||
|
#
|
||||||
|
## TODO
|
||||||
|
#func _save_history(filepath) -> int:
|
||||||
|
# var file := File.new()
|
||||||
|
# var err := file.open(filepath, File.WRITE)
|
||||||
|
# if err == OK:
|
||||||
|
# for line in _history:
|
||||||
|
# assert(line is String)
|
||||||
|
# file.store_line(line)
|
||||||
|
# file.close()
|
||||||
|
# return err
|
12
examples/readline/readline.gd
Normal file
12
examples/readline/readline.gd
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
extends "res://addons/godot_xterm/terminal.gd"
|
||||||
|
|
||||||
|
const Readline = preload("res://addons/godot_xterm/util/readline.gd")
|
||||||
|
|
||||||
|
var rl: Readline
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
rl = Readline.new(self)
|
||||||
|
while true:
|
||||||
|
var line: String = yield(rl.readline("Enter something (anything): "), "completed")
|
||||||
|
write("\r\nYou entered: %s\r\n" % line)
|
13
examples/readline/readline.tscn
Normal file
13
examples/readline/readline.tscn
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[gd_scene load_steps=2 format=2]
|
||||||
|
|
||||||
|
[ext_resource path="res://examples/readline/readline.gd" type="Script" id=1]
|
||||||
|
|
||||||
|
[node name="Terminal" type="Control"]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
rect_pivot_offset = Vector2( -194.759, 935.803 )
|
||||||
|
focus_mode = 2
|
||||||
|
script = ExtResource( 1 )
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": false
|
||||||
|
}
|
196
test/unit/readline.test.gd
Normal file
196
test/unit/readline.test.gd
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
extends "res://addons/gut/test.gd"
|
||||||
|
|
||||||
|
|
||||||
|
class TestReadline:
|
||||||
|
extends "res://addons/gut/test.gd"
|
||||||
|
const Readline = preload("res://addons/godot_xterm/util/readline.gd")
|
||||||
|
const Terminal = preload("res://addons/godot_xterm/terminal.gd")
|
||||||
|
|
||||||
|
var rl: Readline
|
||||||
|
var terminal: Terminal
|
||||||
|
var thread: Thread
|
||||||
|
|
||||||
|
func before_each():
|
||||||
|
terminal = Terminal.new()
|
||||||
|
add_child_autoqfree(terminal)
|
||||||
|
rl = Readline.new(terminal)
|
||||||
|
thread = Thread.new()
|
||||||
|
|
||||||
|
func after_each():
|
||||||
|
if thread.is_active():
|
||||||
|
thread.wait_to_finish()
|
||||||
|
|
||||||
|
func press_keys(keys):
|
||||||
|
yield(get_tree(), "idle_frame")
|
||||||
|
for key in keys:
|
||||||
|
terminal.call_deferred("emit_signal", "key_pressed", key[0], key[1])
|
||||||
|
|
||||||
|
func key(string: String) -> Array:
|
||||||
|
var event = InputEventKey.new()
|
||||||
|
|
||||||
|
if string.length() == 1:
|
||||||
|
event.unicode = ord(string)
|
||||||
|
|
||||||
|
match string:
|
||||||
|
"\r":
|
||||||
|
event.scancode = KEY_ENTER
|
||||||
|
"\b":
|
||||||
|
event.scancode = KEY_BACKSPACE
|
||||||
|
|
||||||
|
return [string, event]
|
||||||
|
|
||||||
|
|
||||||
|
class TestBasic:
|
||||||
|
extends TestReadline
|
||||||
|
|
||||||
|
func test_immediate_return():
|
||||||
|
thread.start(self, "press_keys", [key("\r")])
|
||||||
|
var line = yield(rl.readline("hello> "), "completed")
|
||||||
|
assert_eq(line, "")
|
||||||
|
|
||||||
|
func test_basic_input():
|
||||||
|
thread.start(
|
||||||
|
self,
|
||||||
|
"press_keys",
|
||||||
|
[
|
||||||
|
key("t"),
|
||||||
|
key("e"),
|
||||||
|
key("s"),
|
||||||
|
key("t"),
|
||||||
|
key("\r"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var line = yield(rl.readline("test> "), "completed")
|
||||||
|
assert_eq(line, "test")
|
||||||
|
|
||||||
|
func test_basic_input_no_prompt():
|
||||||
|
thread.start(
|
||||||
|
self,
|
||||||
|
"press_keys",
|
||||||
|
[
|
||||||
|
key("o"),
|
||||||
|
key("k"),
|
||||||
|
key("\r"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var line = yield(rl.readline(), "completed")
|
||||||
|
assert_eq(line, "ok")
|
||||||
|
|
||||||
|
func test_basic_input_empty_prompt():
|
||||||
|
thread.start(
|
||||||
|
self,
|
||||||
|
"press_keys",
|
||||||
|
[
|
||||||
|
key("h"),
|
||||||
|
key("i"),
|
||||||
|
key("\r"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var line = yield(rl.readline(""), "completed")
|
||||||
|
assert_eq(line, "hi")
|
||||||
|
|
||||||
|
func test_backspace_in_line():
|
||||||
|
thread.start(
|
||||||
|
self,
|
||||||
|
"press_keys",
|
||||||
|
[
|
||||||
|
key("a"),
|
||||||
|
key("b"),
|
||||||
|
key("c"),
|
||||||
|
key("\b"),
|
||||||
|
key("d"),
|
||||||
|
key("\r"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var line = yield(rl.readline("> "), "completed")
|
||||||
|
assert_eq(line, "abd")
|
||||||
|
|
||||||
|
func test_backspace_to_prompt():
|
||||||
|
thread.start(
|
||||||
|
self,
|
||||||
|
"press_keys",
|
||||||
|
[
|
||||||
|
key("a"),
|
||||||
|
key("\b"),
|
||||||
|
key("\b"),
|
||||||
|
key("\b"),
|
||||||
|
key("\b"),
|
||||||
|
key("b"),
|
||||||
|
key("\r"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var line = yield(rl.readline("aprompt> "), "completed")
|
||||||
|
var buffer = terminal.copy_all()
|
||||||
|
assert_eq(buffer.strip_edges(false, true), "aprompt>")
|
||||||
|
assert_eq(line, "b")
|
||||||
|
|
||||||
|
func test_multi_line():
|
||||||
|
var keys = []
|
||||||
|
for _i in range(terminal.cols * 3):
|
||||||
|
keys.append(key("a"))
|
||||||
|
keys.append(key("\r"))
|
||||||
|
|
||||||
|
thread.start(self, "press_keys", keys)
|
||||||
|
var line = yield(rl.readline("> "), "completed")
|
||||||
|
assert_eq(line, "a".repeat(terminal.cols * 3))
|
||||||
|
|
||||||
|
func test_backspace_multi_line():
|
||||||
|
var keys = []
|
||||||
|
for _i in range(terminal.cols):
|
||||||
|
keys.append(key("a"))
|
||||||
|
for _j in range(terminal.cols):
|
||||||
|
keys.append(key("b"))
|
||||||
|
for _k in range(terminal.cols + 5):
|
||||||
|
keys.append(key("\b"))
|
||||||
|
keys.append(key("f"))
|
||||||
|
keys.append(key("\r"))
|
||||||
|
|
||||||
|
thread.start(self, "press_keys", keys)
|
||||||
|
var line = yield(rl.readline("> "), "completed")
|
||||||
|
assert_eq(line, "a".repeat(terminal.cols - 5) + "f")
|
||||||
|
var buffer = terminal.copy_all()
|
||||||
|
assert(buffer.strip_edges(false, true), "> " + "a".repeat(terminal.cols - 5) + "f")
|
||||||
|
|
||||||
|
#class TestHistory:
|
||||||
|
# extends TestReadline
|
||||||
|
#
|
||||||
|
# func test_add_history():
|
||||||
|
# rl.history = ["1", "2", "3"]
|
||||||
|
# rl.add_history("New line")
|
||||||
|
# assert_eq(rl.history, ["1", "2", "3", "New line"])
|
||||||
|
#
|
||||||
|
# func test_add_history_max_len():
|
||||||
|
# rl.history = ["1", "2", "3"]
|
||||||
|
# rl.history_max_len = 3
|
||||||
|
# rl.add_history("New line")
|
||||||
|
# assert_eq(rl.history, ["2", "3", "New line"])
|
||||||
|
#
|
||||||
|
# func test_add_history_0_max_len():
|
||||||
|
# rl.history = []
|
||||||
|
# rl.history_max_len = 0
|
||||||
|
# rl.add_history("New line")
|
||||||
|
# assert_eq(rl.history, [])
|
||||||
|
#
|
||||||
|
# func test_equal_max_len():
|
||||||
|
# rl.history = ["1", "2", "3"]
|
||||||
|
# rl.history_max_len = 3
|
||||||
|
# assert_eq(rl.history, ["1", "2", "3"])
|
||||||
|
#
|
||||||
|
# func test_larger_max_len():
|
||||||
|
# rl.history = ["1", "2", "3"]
|
||||||
|
# rl.history_max_len = 4
|
||||||
|
# assert_eq(rl.history, ["1", "2", "3"])
|
||||||
|
#
|
||||||
|
# func test_smaller_max_len():
|
||||||
|
# rl.history = ["1", "2", "3"]
|
||||||
|
# rl.history_max_len = 2
|
||||||
|
# assert_eq(rl.history, ["2", "3"])
|
||||||
|
# rl.history = ["1", "2", "3"]
|
||||||
|
# rl.history_max_len = 1
|
||||||
|
# assert_eq(rl.history, ["3"])
|
||||||
|
# rl.history = ["1", "2", "3"]
|
||||||
|
# rl.history_max_len = 0
|
||||||
|
# assert_eq(rl.history, [])
|
||||||
|
# rl.history = ["1"]
|
||||||
|
# rl.history_max_len = 0
|
||||||
|
# assert_eq(rl.history, [])
|
Loading…
Reference in a new issue