extends Node class_name TerminalControls @onready var historyContainer = $MarginContainer/ScrollContainer/VBoxContainer @onready var caret = $MarginContainer/ScrollContainer/VBoxContainer/Label/Caret @onready var blink_timer = $MarginContainer/ScrollContainer/VBoxContainer/Label/CaretTimer @export var terminalLine: Label var command: String var directory: String = "~/" var fileSystem: Dictionary = { "documents": { "special": { "notes.txt": "test", "secrets": { "password.txt": "123" } } }, "bin": { "sh": "binary data", } } func _ready() -> void: terminalLine.text = "user@work "+ directory + " " UpdateCaretPos() blink_timer.start() Help() # --- Caret Blinking Logic --- func _on_caret_timer_timeout() -> void: caret.visible = !caret.visible func reset_blink() -> void: caret.visible = true blink_timer.start() # Resets the countdown # --- Input Handling --- func InputChar(input) -> void: var charPos = terminalLine.get_character_bounds(max(0, terminalLine.text.length() - 1)) if input == null: return if charPos.end.x > 1600: pass else: command = input terminalLine.text += command UpdateCaretPos() reset_blink() func InputDelChar() -> void: if terminalLine.text.length() > ("user@work " + directory).length() + 1: terminalLine.text = terminalLine.text.left(-1) UpdateCaretPos() reset_blink() func EnterCommand() -> void: var fullText = terminalLine.text var userInput = fullText.trim_prefix("user@work " + directory).strip_edges() var parts = userInput.to_lower().split(" ") if historyContainer.get_child_count() <24: CreateHistoryEntry(fullText) match parts[0]: "ls", "list": var currentDirData if parts.size() > 1: currentDirData = GetDirAtPath(directory + parts[1]) else: currentDirData = GetDirAtPath(directory) if currentDirData is Dictionary: var list = "" for key in currentDirData.keys(): list += key + " " CreateHistoryEntry(list) else: CreateHistoryEntry("error: Directory not found") "cd", "changedirectory": if parts.size() > 1: var targetPath = parts[1] var newDir = ResolvePath(directory, targetPath) if GetDirAtPath(newDir) != null: directory = newDir else: CreateHistoryEntry("cd: no such directory: " + targetPath) else: directory = "~/" "cat", "view": if parts.size() > 1: RetrieveData(parts[1]) else: CreateHistoryEntry("usage: cat [filename]") "clear", "cls": for child in historyContainer.get_children(): if child != terminalLine: child.queue_free() "help": Help() "": pass _: CreateHistoryEntry("Command not found: " + userInput) terminalLine.text = "user@work "+ directory + " " UpdateCaretPos() GetBottomScroll() # --- History and FileSystem Helpers --- func CreateHistoryEntry(content: String) -> void: var label = Label.new() label.text = content label.label_settings = LabelSettings.new() label.label_settings.font_size = 42 label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.custom_minimum_size = Vector2(1400, 0) # #if terminalLine.text.length() > 200: #for child in historyContainer.get_children(): #if child != terminalLine: #child.queue_free() historyContainer.add_child(label) historyContainer.move_child(label, historyContainer.get_child_count() - 2) func ResolvePath(current: String, target: String) -> String: if target.begins_with("~/"): return target if target.ends_with("/") else target + "/" if target == "..": if current == "~/": return "~/" var parts = current.trim_suffix("/").rsplit("/", true, 1) return parts[0] + "/" var joined = current + target return joined if joined.ends_with("/") else joined + "/" func GetDirAtPath(path: String): if path == "~/" or path == "": return fileSystem var cleanPath = path.trim_prefix("~/").trim_suffix("/") var folders = cleanPath.split("/") var current = fileSystem for folder in folders: if folder == "": continue if current is Dictionary and current.has(folder): if current[folder] is Dictionary: current = current[folder] else: # We hit a file, not a directory return null else: return null return current func RetrieveData(inputPath: String): var targetFileName: String var targetDirData: Dictionary if "/" in inputPath: var pathParts = inputPath.rsplit("/", true, 1) var dirPath = pathParts[0] targetFileName = pathParts[1] var resolvedDirPath = ResolvePath(directory, dirPath) var result = GetDirAtPath(resolvedDirPath) if result is Dictionary: targetDirData = result else: CreateHistoryEntry("cat: " + dirPath + ": No such directory") return else: targetFileName = inputPath targetDirData = GetDirAtPath(directory) if targetDirData.has(targetFileName): var content = targetDirData[targetFileName] if content is Dictionary: CreateHistoryEntry("cat: " + targetFileName + ": Is a directory") else: CreateHistoryEntry(str(content)) else: CreateHistoryEntry("cat: " + targetFileName + ": No such file or directory") func Help(commandName: String = "default"): match commandName: "default": CreateHistoryEntry("--- AVAILABLE COMMANDS --- ls (or list) [folder] : List all files and directories/folders cd [folder] : Change directory (use '..' to go up a directory/folder) cat (or view) [file] : Read the contents of a file clear (or cls) : Clear the terminal screen help : Show this menu -------------------------") func ScrollUp(): var scroll: ScrollContainer = $MarginContainer/ScrollContainer await get_tree().create_timer(.0001).timeout scroll.set_deferred("scroll_vertical", scroll.get_v_scroll_bar().value - 10 ) call("ScrollUp") func ScrollDown(): var scroll: ScrollContainer = $MarginContainer/ScrollContainer await get_tree().create_timer(.0001).timeout scroll.set_deferred("scroll_vertical", scroll.get_v_scroll_bar().value + 10 ) call("ScrollDown") func GetBottomScroll(): var scroll: ScrollContainer = $MarginContainer/ScrollContainer var scrollMax: float = scroll.get_v_scroll_bar().max_value await get_tree().create_timer(.01).timeout scroll.set_deferred("scroll_vertical", scrollMax) #scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_SHOW_NEVER call_deferred("GetBottomScroll") func UpdateCaretPos(): var last_char_index = terminalLine.text.length() - 1 var char_rect = terminalLine.get_character_bounds(max(0, last_char_index)) caret.position.x = char_rect.end.x + 1 caret.position.y = char_rect.position.y