package moe.nea89.website import kotlinx.browser.document import kotlinx.html.dom.append import kotlinx.html.js.pre import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLPreElement import org.w3c.dom.events.KeyboardEvent import kotlin.collections.set class KConsole( private val root: HTMLElement, private val text: HTMLPreElement, private val fileSystem: KFileSystem?, ) { val fileAccessor = fileSystem?.let { FileAccessor(it) } companion object { val shlexRegex = """"([^"\\]+|\\.)+"|([^ "'\\]+|\\.)+|'([^'\\]+|\\.)+'""".toRegex() fun createFor(element: HTMLElement, fileSystem: KFileSystem? = null): KConsole { val text = element.append.pre() element.classList.add(Styles.consoleClass) val console = KConsole(element, text, fileSystem) document.body!!.onkeydown = console::keydown console.addLine("Starting up terminal.") console.rerender() return console } } enum class ConsoleState { SHELLPROMPT, IN_PROGRAM } var state = ConsoleState.SHELLPROMPT val lines = mutableListOf() var input: String = "" fun addLines(newLines: List) { lines.addAll(newLines) } fun addMultilineText(text: String) { addLines(text.split("\n")) } fun addLine(line: String) { lines.add(line) } fun scrollDown() { text.lastElementChild?.scrollIntoView() } fun rerender() { var view = lines.joinToString(separator = "\n") if (state == ConsoleState.SHELLPROMPT) { view += "\n${'$'} $input" } text.innerText = view } fun registerCommand(command: Command) { command.aliases.forEach { commands[it] = command } commands[command.name] = command } val commands = mutableMapOf() fun executeCommand(commandLine: String) { val parts = shlex(commandLine) if (parts.isNullOrEmpty()) { addLine("Syntax Error") return } val command = parts[0] println("Running command: $command") val arguments = parts.drop(1) val commandThing = commands[command] if (commandThing == null) { addLine("Unknown command") return } ShellExecutionContext.run(this, commandThing, command, arguments) } @OptIn(ExperimentalStdlibApi::class) fun shlex(command: String): List? { var i = 0 val parts = mutableListOf() while (i < command.length) { val match = shlexRegex.matchAt(command, i) if (match == null) { println("Could not shlex: $command") return null } // TODO: Proper string unescaping parts.add(match.groupValues.drop(1).firstOrNull { it != "" } ?: "") i += match.value.length while (command[i] == ' ' && i < command.length) i++ } return parts } fun keydown(event: KeyboardEvent) { if (event.altKey || event.ctrlKey || event.metaKey) return if (event.isComposing || event.keyCode == 229) return if (state != ConsoleState.SHELLPROMPT) return when (event.key) { "Enter" -> { val toExecute = input addLine("${'$'} $toExecute") input = "" executeCommand(toExecute) } "Backspace" -> input = input.substring(0, input.length - 1) else -> if (event.key.length == 1 || event.key.any { it !in 'a'..'z' && it !in 'A'..'Z' }) input += event.key } rerender() scrollDown() } }