xsivgnazlixs/addons/dev_console/dev_console.ts
2025-05-02 19:10:14 -05:00

169 lines
4 KiB
TypeScript

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<string>
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, string>): 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(' '))
}
}