import { Callable, Callable1, Control, Expression, GArray, GDictionary, GError, InputEvent, RichTextLabel, Variant } from 'godot' import { export_, 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 Control { @export_(Variant.Type.TYPE_BOOL) private pause_when_open: boolean = true @onready("VBoxContainer/Output") private output!: RichTextLabel @onready("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() } async _show() { await this.get_tree().process_frame.as_promise() if (this.pause_when_open) { this.get_tree().paused = true } this.input.text_submitted.connect(this.parse_callable) this.input.grab_focus() } _hide() { if (this.pause_when_open) { this.get_tree().paused = false } 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()) } } clear() { this.output.clear() } echo(...args: any) { this._print(args.map((arg: any) => arg.toString()).join(' ')) } }