@tool extends VBoxContainer signal command_submitted(command: String) signal focus_requested @export var session_id := "rov" @export var tab_label := "ROV" @export var shortcut: Key = KEY_F5 @export var prompt := "rov $" @export var read_only := false @export var introduction := "ROV shell disconnected." @export var max_scrollback_characters := 128000 var connected := false var _buffer := "" var _history: Array[String] = [] var _history_index := 0 var _draft := "" @onready var output: RichTextLabel = $Output @onready var command_input: LineEdit = $Input/Command @onready var filter_input: LineEdit = $Filter/Text func _ready() -> void: $Input.visible = not read_only $Filter.visible = read_only $Input/Prompt.text = prompt _buffer = introduction + "\n" output.text = _buffer if Engine.is_editor_hint(): return command_input.text_submitted.connect(submit_command) command_input.gui_input.connect(_history_input) filter_input.text_changed.connect(func(_text: String) -> void: _render_output()) $Status/Clear.pressed.connect(clear_output) $Status/Follow.toggled.connect(func(enabled: bool) -> void: output.scroll_following = enabled if enabled: output.scroll_to_line(maxi(output.get_line_count() - 1, 0)) ) for control in [output, command_input, filter_input, $Status/Clear, $Status/Follow]: control.gui_input.connect(func(event: InputEvent) -> void: if event is InputEventMouseButton and event.pressed: focus_requested.emit() ) func preferred_input() -> Control: return filter_input if read_only else command_input func set_connection(value: bool, message: String = "") -> void: connected = value $Status/Connection.text = message if not message.is_empty() else ("Connected" if value else "Not connected") func submit_command(command: String) -> bool: if read_only or command.strip_edges().is_empty(): return false if _history.is_empty() or _history.back() != command: _history.append(command) if _history.size() > 200: _history.pop_front() _history_index = _history.size() _draft = "" append_output(prompt + " " + command + "\n") command_input.clear() command_submitted.emit(command) return true func append_output(text: String) -> void: _buffer += text var truncated := _buffer.length() > max_scrollback_characters if truncated: _buffer = _buffer.right(max_scrollback_characters) var newline := _buffer.find("\n") if newline >= 0 and newline < _buffer.length() - 1: _buffer = _buffer.substr(newline + 1) if truncated or (read_only and not filter_input.text.is_empty()): _render_output() else: output.add_text(text) func clear_output() -> void: _buffer = "" output.clear() func _render_output() -> void: var scroll := output.get_v_scroll_bar().value if read_only and not filter_input.text.is_empty(): var matches := PackedStringArray() for line in _buffer.split("\n"): if line.to_lower().contains(filter_input.text.to_lower()): matches.append(line) output.text = "\n".join(matches) else: output.text = _buffer if not $Status/Follow.button_pressed: output.get_v_scroll_bar().set_deferred("value", scroll) func _history_input(event: InputEvent) -> void: if not event is InputEventKey or not event.pressed or event.ctrl_pressed or event.alt_pressed or event.meta_pressed: return if event.keycode not in [KEY_UP, KEY_DOWN] or _history.is_empty(): return if _history_index == _history.size(): _draft = command_input.text _history_index = clampi(_history_index + (-1 if event.keycode == KEY_UP else 1), 0, _history.size()) command_input.text = _draft if _history_index == _history.size() else _history[_history_index] command_input.caret_column = command_input.text.length() command_input.accept_event()