summaryrefslogtreecommitdiff
path: root/src/jsMain/kotlin/moe/nea89/website/KConsole.kt
diff options
context:
space:
mode:
Diffstat (limited to 'src/jsMain/kotlin/moe/nea89/website/KConsole.kt')
-rw-r--r--src/jsMain/kotlin/moe/nea89/website/KConsole.kt173
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()
+ }
+}