import { Callable, Callable1, CanvasLayer, Expression, GArray, GDictionary, GError, InputEvent, RichTextLabel } from 'godot' import { onready } from 'godot.annotations' import AutocompleteLine from './autocomplete_line' import { GArrayEnumerator } from '../../src/collection/enumerable' import { Enumerable } from '../enumerable-ts/src/index' export const Action = { Toggle: '_dev_console_toggle', Cancel: 'ui_cancel', Up: 'ui_up', Down: 'ui_down', TextComplete: 'ui_text_completion_replace', FocusNext: 'ui_focus_next', FocusPrevious: 'ui_focus_prev', } as const export default class DevConsole extends CanvasLayer { @onready("Container/VBoxContainer/Output") private output!: RichTextLabel @onready("Container/VBoxContainer/Input") private input!: AutocompleteLine private history: string[] = [] private expression = new Expression() private parse_callable!: Callable1 private history_index: number = -1 _ready(): void { this._toggle() const method_names = Enumerable.from(new GArrayEnumerator(this.get_method_list())) .map((x: GDictionary) => x.get('name')) .filter((x: string) => !x.startsWith('_')) this.input.autocomplete_list = method_names.toArray() this.parse_callable = Callable.create( this, this.submit_command ) } _input(event: InputEvent): void { if (event.is_action_pressed(Action.Toggle)) { this._toggle() return } if (!this.visible) { return } if (event.is_action_pressed(Action.Cancel)) { this._cancel() } else if (event.is_action_pressed(Action.Up)) { this._move_history(-1) } else if (event.is_action_pressed(Action.Down)) { this._move_history(1) } } _reset() { this.history_index = -1 } _cancel() { this._reset() if (this.input.has_focus()) { this.input.release_focus() } else { this._hide() } } async _move_history(direction: (-1 | 1)) { this.history_index = (this.history_index + direction) % this.history.length if (this.history_index < 0) { this.history_index = this.history.length - 1 } this.input.text = this.history[this.history_index] await this.get_tree().process_frame.as_promise() this.input.set_caret_to_end() } _autocomplete() { this.input.autocomplete() } async _show() { await this.get_tree().process_frame.as_promise() this.input.text_submitted.connect(this.parse_callable) this.input.grab_focus() } _hide() { this.input.text_submitted.disconnect(this.parse_callable) this.input.release_focus() } async _toggle() { this._reset() this.visible = !this.visible if (this.visible) { this._show() } else { this._hide() } } print(value: string) { this.output.append_text(value) this.output.append_text('\n') } wrap_in_tag(text: string, tag: [string, string]): string { return `[${tag[0]}=${tag[1]}]${text}[/${tag[0]}]` } format(text: string, tags: Record): string { return Object.entries(tags).reduce(this.wrap_in_tag, text) } print_error(error: string) { this.print(this.format(error, { color: 'red' })) } echo(command: string) { this.print(this.format(`> ${command}`, { color: 'white' })) } submit_command(command: string) { this.input.clear() this.history.push(command) this.echo(command) const error = this.expression.parse(command) if (error !== GError.OK) { this.print_error(this.expression.get_error_text()) return } const result = this.expression.execute(new GArray(), this) if (this.expression.has_execute_failed()) { this.print_error(this.expression.get_error_text()) } else { this.print(result.toString()) } } }