323 lines
8.6 KiB
GDScript
323 lines
8.6 KiB
GDScript
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
|
|
@onready var ruler: Label = $"caret-ruler"
|
|
@onready var scroll: ScrollContainer = $MarginContainer/ScrollContainer
|
|
@export var terminalLine: RichTextLabel
|
|
|
|
var terminalHistory: Array[String] = []
|
|
var historyIndex: int = -1
|
|
|
|
var cursorIndexFromEnd: int = 0
|
|
|
|
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 + " "
|
|
blink_timer.start()
|
|
Help()
|
|
UpdateCaretPos()
|
|
|
|
# --- Caret Blinking Logic ---
|
|
func _on_caret_timer_timeout() -> void:
|
|
caret.visible = !caret.visible
|
|
|
|
func ResetBlink() -> void:
|
|
caret.visible = true
|
|
blink_timer.start() # Resets the countdown
|
|
|
|
# --- Input Handling ---
|
|
func InputChar(input) -> void:
|
|
if input == null:
|
|
return
|
|
|
|
var fullText = terminalLine.text
|
|
var insertPos = fullText.length() - cursorIndexFromEnd
|
|
|
|
var before = fullText.left(insertPos)
|
|
var after = fullText.right(cursorIndexFromEnd)
|
|
|
|
terminalLine.text = before + input + after
|
|
|
|
UpdateCaretPos()
|
|
|
|
await get_tree().physics_frame
|
|
ResetBlink()
|
|
|
|
func InputDelChar() -> void:
|
|
var minLength = ("user@work " + directory).length() + 1
|
|
if terminalLine.text.length() <= minLength:
|
|
return
|
|
|
|
var fullText = terminalLine.text
|
|
var deletePos = fullText.length() - cursorIndexFromEnd
|
|
|
|
if deletePos > minLength:
|
|
var before = fullText.left(deletePos - 1)
|
|
var after = fullText.right(cursorIndexFromEnd)
|
|
|
|
terminalLine.text = before + after
|
|
UpdateCaretPos()
|
|
|
|
await get_tree().physics_frame
|
|
ResetBlink()
|
|
|
|
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)
|
|
|
|
if userInput != "":
|
|
terminalHistory.append(userInput)
|
|
historyIndex = terminalHistory.size()
|
|
cursorIndexFromEnd = 0
|
|
|
|
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 = ""
|
|
var file: String
|
|
for key in currentDirData.keys():
|
|
file = key
|
|
if file.ends_with(".txt"):
|
|
list += key + " "
|
|
else:
|
|
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()
|
|
|
|
|
|
func MoveCursorLeft():
|
|
var full_text = terminalLine.get_parsed_text()
|
|
var last_newline_pos = full_text.rfind("\n")
|
|
|
|
# The prompt starts after the last newline (or at 0 if it's the first line)
|
|
var prompt_start_index = last_newline_pos + 1 if last_newline_pos != -1 else 0
|
|
var prompt_length = ("user@work " + directory + " ").length()
|
|
|
|
# The absolute index we aren't allowed to go before
|
|
var min_allowed_index = prompt_start_index + prompt_length
|
|
|
|
# Calculate current target index
|
|
var current_target = full_text.length() - cursorIndexFromEnd
|
|
|
|
if current_target > min_allowed_index:
|
|
cursorIndexFromEnd += 1
|
|
UpdateCaretPos()
|
|
ResetBlink()
|
|
|
|
func MoveCursorRight():
|
|
|
|
cursorIndexFromEnd = clampi(cursorIndexFromEnd - 1, 0, terminalLine.text.length())
|
|
|
|
UpdateCaretPos()
|
|
ResetBlink()
|
|
|
|
# --- History and FileSystem Helpers ---
|
|
|
|
func NavigateHistory(direction: int):
|
|
if terminalHistory.is_empty():
|
|
return
|
|
|
|
historyIndex -= direction
|
|
|
|
if historyIndex >= terminalHistory.size():
|
|
historyIndex = terminalHistory.size()
|
|
terminalLine.text = "user@work "+ directory + " "
|
|
else:
|
|
historyIndex = clamp(historyIndex, 0, terminalHistory.size() - 1)
|
|
var historyCommand = terminalHistory[historyIndex]
|
|
terminalLine.text = "user@work "+ directory + " " + historyCommand
|
|
|
|
UpdateCaretPos()
|
|
|
|
func CreateHistoryEntry(content: String) -> void:
|
|
|
|
var rtl = RichTextLabel.new()
|
|
rtl.bbcode_enabled = true
|
|
rtl.fit_content = true
|
|
rtl.text = content
|
|
rtl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
rtl.custom_minimum_size = Vector2(1400, 0)
|
|
rtl.add_theme_font_size_override("normal_font_size", 42)
|
|
|
|
historyContainer.add_child(rtl)
|
|
historyContainer.move_child(rtl, 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 --- \n
|
|
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
|
|
\n
|
|
")
|
|
|
|
func ScrollUp():
|
|
|
|
await get_tree().physics_frame
|
|
scroll.set_deferred("scroll_vertical", scroll.get_v_scroll_bar().value - 100 )
|
|
|
|
|
|
func ScrollDown():
|
|
|
|
await get_tree().physics_frame
|
|
scroll.set_deferred("scroll_vertical", scroll.get_v_scroll_bar().value + 100 )
|
|
|
|
|
|
|
|
func GetBottomScroll():
|
|
var scrollMax: float = scroll.get_v_scroll_bar().max_value
|
|
|
|
await get_tree().create_timer(.01).timeout
|
|
scroll.set_deferred("scroll_vertical", scrollMax)
|
|
|
|
func UpdateCaretPos():
|
|
await get_tree().physics_frame
|
|
var visibleText = terminalLine.get_parsed_text()
|
|
|
|
ruler.text = visibleText
|
|
ruler.custom_minimum_size.x = terminalLine.size.x
|
|
ruler.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
|
|
var totalLen = visibleText.length()
|
|
var targetIndex = clampi(totalLen - cursorIndexFromEnd, 0, totalLen)
|
|
|
|
if totalLen == 0:
|
|
caret.position = Vector2.ZERO
|
|
return
|
|
|
|
var lastCharBounds = ruler.get_character_bounds(max(0, totalLen - 1))
|
|
var targetCharBounds = ruler.get_character_bounds(targetIndex)
|
|
|
|
if cursorIndexFromEnd == 0:
|
|
caret.position.x = lastCharBounds.end.x
|
|
caret.position.y = lastCharBounds.position.y
|
|
else:
|
|
caret.position.x = targetCharBounds.position.x
|
|
caret.position.y = targetCharBounds.position.y
|
|
|
|
caret.position.x += 1
|