ajhahn.de
← Theria
GDScript 217 lines
@tool
extends Control

# I'm probably going to put this back in later and I don't want to create it
# again.  Yeah, yeah, yeah.
# class DotsAnimator:
# 	var text = ''
# 	var dot = '.'
# 	var max_dots = 3
# 	var dot_delay = .5

# 	var _anim_text = ''
# 	var _elapsed_time = 0.0
# 	var _cur_dots = 0

# 	func get_animated_text():
# 		return _anim_text

# 	func add_time(delta):
# 		_elapsed_time += delta
# 		if(_elapsed_time > dot_delay):
# 			_elapsed_time = 0
# 			_cur_dots += 1
# 			if(_cur_dots > max_dots):
# 				_cur_dots = 0

# 			_anim_text = text.rpad(text.length() + _cur_dots, dot)


var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')

@onready var btn_kill_it = $BgControl/VBox/Kill
@onready var bg_control = $BgControl

var _pipe_results = {}
var _debug_mode = false
var _std_thread : Thread
var _escape_regex : RegEx = RegEx.new()
var _text_buffer = ''

var bottom_panel = null :
	set(val):
		bottom_panel = val
		bottom_panel.resized.connect(_on_bottom_panel_resized)
var blocking_mode = "Blocking"
var additional_arguments = []
var remove_escape_characters = true
@export var bg_color = Color.WHITE:
	set(val):
		bg_color = val
		if(is_inside_tree()):
			bg_control.get("theme_override_styles/panel").bg_color = bg_color


func _debug_ready():
	_debug_mode = true
	additional_arguments = ['-gselect', 'test_awaiter.gd', '-gconfig', 'res://.gutconfig.json'] # '-gunit_test_name', 'test_can_clear_spies'
	blocking_mode = "NonBlocking"
	run_tests()


func _ready():
	_escape_regex.compile("\\x1b\\[[0-9;]*m")
	btn_kill_it.visible = false

	if(get_parent() == get_tree().root):
		_debug_ready.call_deferred()
	bg_color = bg_color


func _process(_delta: float) -> void:
	if(_pipe_results != {}):
		if(!OS.is_process_running(_pipe_results.pid)):
			_end_non_blocking()


# ----------
# Private
# ----------
func _center_me():
	position = get_parent().size / 2.0 - size / 2.0


func _output_text(text, should_scroll = true):
	if(_debug_mode):
		print(text)
	else:
		if(remove_escape_characters):
			text = _escape_regex.sub(text, '', true)

		if(bottom_panel != null):
			bottom_panel.add_output_text(text)
			if(should_scroll):
				_scroll_output_pane(-1)
		else:
			_text_buffer += text


func _scroll_output_pane(line):
	if(!_debug_mode and bottom_panel != null):
		var txt_ctrl = bottom_panel.get_text_output_control().get_rich_text_edit()
		if(line == -1):
			line = txt_ctrl.get_line_count()
		txt_ctrl.scroll_vertical = line


func _add_arguments_to_output():
	if(additional_arguments.size() != 0):
		_output_text(
			str("Run Mode arguments: ", ' '.join(additional_arguments), "\n\n")
		)


func _load_json():
	if(_debug_mode):
		pass # could load file and print it if we want.
	elif(bottom_panel != null):
		bottom_panel.load_result_json()


func _run_blocking(options):
	btn_kill_it.visible = false
	var output = []
	await get_tree().create_timer(.1).timeout

	OS.execute(OS.get_executable_path(), options, output, true)

	_output_text(output[0])
	_add_arguments_to_output()
	_scroll_output_pane(-1)

	_load_json()
	queue_free()


func _read_non_blocking_stdio():
	while(OS.is_process_running(_pipe_results.pid)):
		while(_pipe_results.stderr.get_length() > 0):
			_output_text(_pipe_results.stderr.get_line() + "\n")

		while(_pipe_results.stdio.get_length() > 0):
			_output_text(_pipe_results.stdio.get_line() + "\n")

		# without this, things start to lock up.
		await get_tree().process_frame


func _run_non_blocking(options):
	_pipe_results = OS.execute_with_pipe(OS.get_executable_path(), options, false)
	_std_thread = Thread.new()
	_std_thread.start(_read_non_blocking_stdio)
	btn_kill_it.visible = true


func _end_non_blocking():
	_add_arguments_to_output()
	_scroll_output_pane(-1)

	_load_json()

	_pipe_results = {}
	_std_thread.wait_to_finish()
	_std_thread = null
	queue_free()
	if(_debug_mode):
		get_tree().quit()



# ----------------
# Events
# ----------------
func _on_kill_pressed() -> void:
	if(_pipe_results != {} and OS.is_process_running(_pipe_results.pid)):
		OS.kill(_pipe_results.pid)
		btn_kill_it.visible = false


func _on_color_rect_gui_input(event: InputEvent) -> void:
	if(event is InputEventMouseMotion):
		if(event.button_mask == MOUSE_BUTTON_MASK_LEFT):
			position += event.relative


func _on_bottom_panel_resized():
	_center_me()


# ----------------
# Public
# ----------------
func run_tests():
	_center_me()

	var options = ["-s", "res://addons/gut/gut_cmdln.gd", "-graie", "-gdisable_colors",
		"-gconfig", GutEditorGlobals.editor_run_gut_config_path]
	options.append_array(additional_arguments)

	if(blocking_mode == 'Blocking'):
		_run_blocking(options)
	else:
		_run_non_blocking(options)


func get_godot_help():
	_text_buffer = ''
	var options = ["--help", "--headless"]
	await _run_blocking(options)
	return _text_buffer


func get_gut_help():
	_text_buffer = ''
	var options = ["-s", "res://addons/gut/gut_cmdln.gd", "-gh", "--headless"]
	await _run_blocking(options)
	return _text_buffer