diff options
Diffstat (limited to 'src/jsMain/kotlin/moe/nea89/website/KConsole.kt')
-rw-r--r-- | src/jsMain/kotlin/moe/nea89/website/KConsole.kt | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/src/jsMain/kotlin/moe/nea89/website/KConsole.kt b/src/jsMain/kotlin/moe/nea89/website/KConsole.kt new file mode 100644 index 0000000..4a51219 --- /dev/null +++ b/src/jsMain/kotlin/moe/nea89/website/KConsole.kt @@ -0,0 +1,173 @@ +package moe.nea89.website + +import kotlinx.browser.document +import kotlinx.dom.addClass +import kotlinx.html.dom.append +import kotlinx.html.dom.create +import kotlinx.html.input +import kotlinx.html.js.p +import kotlinx.html.js.pre +import kotlinx.html.js.span +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLParagraphElement +import org.w3c.dom.HTMLPreElement +import org.w3c.dom.events.KeyboardEvent +import styled.injectGlobal +import kotlin.collections.set + + +class KConsole( + val root: HTMLElement, + val text: HTMLPreElement, + val prompt: HTMLElement, + fileSystem: KFileSystem?, +) { + + + val fileAccessor = fileSystem?.let { FileAccessor(it) } + var PS1: KConsole.() -> String = { "$" } + + companion object { + + init { + injectGlobal(Styles.global) + } + + val shlexRegex = + """"([^"\\]+|\\.)+"|([^ "'\\]+|\\.)+|'([^'\\]+|\\.)+'""".toRegex() + + fun createFor(element: HTMLElement, fileSystem: KFileSystem? = null): KConsole { + val text = element.append.pre() + val prompt = text.append.p() + prompt.addClass(Styles.promptClass) + element.classList.add(Styles.consoleClass) + val console = KConsole(element, text, prompt, fileSystem) + val inp = element.append.input() + inp.hidden = true + inp.focus() + document.body!!.onkeydown = console::keydown + console.rerender() + return console + } + } + + enum class ConsoleState { + SHELLPROMPT, + IN_PROGRAM + } + + var state = ConsoleState.SHELLPROMPT + + var input: String = "" + + fun addLines(newLines: List<String>) { + newLines.forEach { addLine(it) } + } + + fun addMultilineText(text: String) { + addLines(text.split("\n")) + } + + fun addLine(vararg elements: Any) { + addLine(document.create.p().apply { + elements.forEach { + when (it) { + is HTMLElement -> append(it) + is ColoredElement -> append(document.create.span().also { el -> + el.style.color = it.color.color.toString() + el.append(it.text) + }) + + is String -> append(it) + else -> throw RuntimeException("Unknown element") + } + } + }) + } + + private fun addLine(element: HTMLParagraphElement) { + text.insertBefore(element, prompt) + } + + fun rerender() { + if (state == KConsole.ConsoleState.SHELLPROMPT) { + prompt.innerText = "${PS1.invoke(this)} $input" + } else { + prompt.innerText = "" + } + } + + fun scrollDown() { + text.lastElementChild?.scrollIntoView() + } + + fun registerCommand(command: Command) { + command.aliases.forEach { + commands[it] = command + } + commands[command.name] = command + } + + val commands = mutableMapOf<String, Command>() + + fun executeCommand(commandLine: String) { + val parts = shlex(commandLine) + if (parts == null) { + addLine("Syntax Error") + return + } + if (parts.isEmpty()) { + 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) + scrollDown() + } + + fun shlex(command: String): List<String>? { + var i = 0 + val parts = mutableListOf<String>() + 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("${PS1.invoke(this)} $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 + } + event.preventDefault() + rerender() + scrollDown() + } +} |