summaryrefslogtreecommitdiff
path: root/src/jsMain
diff options
context:
space:
mode:
Diffstat (limited to 'src/jsMain')
-rw-r--r--src/jsMain/kotlin/moe/nea89/website/Colored.kt23
-rw-r--r--src/jsMain/kotlin/moe/nea89/website/Command.kt11
-rw-r--r--src/jsMain/kotlin/moe/nea89/website/KConsole.kt173
-rw-r--r--src/jsMain/kotlin/moe/nea89/website/KFiles.kt161
-rw-r--r--src/jsMain/kotlin/moe/nea89/website/ScrollIntoViewOptions.kt7
-rw-r--r--src/jsMain/kotlin/moe/nea89/website/ShellExecutionContext.kt51
-rw-r--r--src/jsMain/kotlin/moe/nea89/website/Styles.kt55
-rw-r--r--src/jsMain/kotlin/moe/nea89/website/util.kt6
8 files changed, 487 insertions, 0 deletions
diff --git a/src/jsMain/kotlin/moe/nea89/website/Colored.kt b/src/jsMain/kotlin/moe/nea89/website/Colored.kt
new file mode 100644
index 0000000..9918c85
--- /dev/null
+++ b/src/jsMain/kotlin/moe/nea89/website/Colored.kt
@@ -0,0 +1,23 @@
+package moe.nea89.website
+
+import kotlinx.css.*
+
+enum class CustomColor(val color: Color) {
+ BLACK(Color("#282a39")),
+ RED(Color("#ff4473")),
+ BLUE(Color("#00fefc")),
+ PURPLE(Color("#6064fe")),
+ GREEN(Color("#4ce080")),
+ WHITE(Color("#efefef")),
+}
+
+data class ColoredElement(
+ val color: CustomColor,
+ val text: String
+)
+
+fun red(text: String) = ColoredElement(CustomColor.RED, text)
+fun blue(text: String) = ColoredElement(CustomColor.BLUE, text)
+fun purple(text: String) = ColoredElement(CustomColor.PURPLE, text)
+fun green(text: String) = ColoredElement(CustomColor.GREEN, text)
+
diff --git a/src/jsMain/kotlin/moe/nea89/website/Command.kt b/src/jsMain/kotlin/moe/nea89/website/Command.kt
new file mode 100644
index 0000000..b8e4675
--- /dev/null
+++ b/src/jsMain/kotlin/moe/nea89/website/Command.kt
@@ -0,0 +1,11 @@
+package moe.nea89.website
+
+data class Command(
+ val name: String,
+ val aliases: Set<String>,
+ val runner: suspend ShellExecutionContext.() -> Unit,
+)
+
+
+fun command(name: String, vararg aliases: String, block: suspend ShellExecutionContext. () -> Unit) =
+ Command(name, aliases.toSet(), block) \ No newline at end of file
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()
+ }
+}
diff --git a/src/jsMain/kotlin/moe/nea89/website/KFiles.kt b/src/jsMain/kotlin/moe/nea89/website/KFiles.kt
new file mode 100644
index 0000000..aad3036
--- /dev/null
+++ b/src/jsMain/kotlin/moe/nea89/website/KFiles.kt
@@ -0,0 +1,161 @@
+package moe.nea89.website
+
+sealed class KFile {
+ /**
+ * Only be empty for the root fs
+ * */
+ var parent: Directory? = null
+ private set
+
+ val name: List<String>
+ get() =
+ parent?.let { it.name + it.files.filter { it.value == this }.keys.first() } ?: emptyList()
+
+ fun linkTo(parent: Directory) {
+ if (this.parent == null)
+ this.parent = parent
+ }
+
+ val fileType: String
+ get() = when (this) {
+ is Directory -> "directory"
+ is Download -> "download"
+ is Image -> "image"
+ is Text -> "text file"
+ }
+
+ data class Text(val text: String) : KFile()
+ data class Image(val url: String) : KFile()
+ data class Download(val url: String) : KFile()
+ data class Directory(val files: Map<String, KFile>) : KFile()
+}
+
+data class KFileSystem(val root: KFile.Directory) {
+ init {
+ if (!verifyHierarchy(root)) {
+ throw RuntimeException("File system had missing links. Use linkTo with the primary parent directory")
+ }
+ }
+
+ private fun verifyHierarchy(el: KFile.Directory): Boolean =
+ el.files.values.all {
+ it.parent == el && (it !is KFile.Directory || verifyHierarchy(it))
+ }
+
+
+ /**
+ * Uses normalized paths
+ * */
+ fun resolve(parts: List<String>): KFile? =
+ parts.fold<String, KFile?>(root) { current, part ->
+ if (part == "." || part == "")
+ current
+ else if (part == "..")
+ current?.parent
+ else if (current is KFile.Directory) {
+ current.files[part]
+ } else
+ null
+ }
+}
+
+
+enum class FSError {
+ ENOENT, EISNOTDIR
+}
+
+class FileAccessor(val fileSystem: KFileSystem, var implicitPushD: Boolean = false) { // TODO implicit pushd support
+ val dirStack = mutableListOf<List<String>>()
+ var currentDir = listOf<String>()
+
+ fun cd(path: String): FSError? {
+ val file = resolve(path) ?: return FSError.ENOENT
+ return when (file) {
+ !is KFile.Directory -> FSError.EISNOTDIR
+ else -> {
+ currentDir = file.name
+ null
+ }
+ }
+ }
+
+ fun resolve(path: String): KFile? {
+ val parts = path.split("/").filter { it.isNotEmpty() && it != "." }
+ return if (path.startsWith("/")) {
+ fileSystem.resolve(parts)
+ } else {
+ fileSystem.resolve(currentDir + parts)
+ }
+ }
+
+ fun pushD() {
+ dirStack.add(currentDir)
+ }
+
+ fun useD(block: () -> Unit) {
+ val d = currentDir
+ try {
+ block()
+ } finally {
+ currentDir = d
+ }
+ }
+
+ fun popD(): Boolean {
+ currentDir = dirStack.removeLastOrNull() ?: return false
+ return true
+ }
+}
+
+@DslMarker
+annotation class KFileDsl
+
+fun fileSystem(block: FileSystemBuilder.() -> Unit): KFileSystem =
+ KFileSystem(FileSystemBuilder().also(block).build())
+
+
+@KFileDsl
+class FileSystemBuilder {
+ private val files = mutableMapOf<String, KFile>()
+
+ fun addNode(name: String, file: KFile): FileSystemBuilder {
+ val parts = name.split("/", limit = 2)
+ if (parts.size != 1) {
+ return addNode(parts[0], FileSystemBuilder().addNode(parts[1], file).build())
+ }
+ if (files.containsKey(name)) {
+ throw RuntimeException("Tried to double set file: $name")
+ }
+ files[name] = file
+ return this
+ }
+
+ infix fun String.text(rawText: String) {
+ addNode(this, KFile.Text(rawText))
+ }
+
+ infix fun String.image(dataUrl: String) {
+ addNode(this, KFile.Image(dataUrl))
+ }
+
+ infix fun String.download(url: String) {
+ addNode(this, KFile.Download(url))
+ }
+
+ operator fun String.invoke(block: FileSystemBuilder.() -> Unit) {
+ addNode(this, FileSystemBuilder().also(block).build())
+ }
+
+ fun build() = KFile.Directory(files).also { dir ->
+ files.values.forEach { file -> file.linkTo(dir) }
+ }
+}
+
+suspend fun ShellExecutionContext.requireFileAccessor(): FileAccessor {
+ val fa = console.fileAccessor
+ if (fa == null) {
+ console.addLine("There is no file accessor present :(")
+ exit()
+ }
+ return fa
+}
diff --git a/src/jsMain/kotlin/moe/nea89/website/ScrollIntoViewOptions.kt b/src/jsMain/kotlin/moe/nea89/website/ScrollIntoViewOptions.kt
new file mode 100644
index 0000000..aab14e2
--- /dev/null
+++ b/src/jsMain/kotlin/moe/nea89/website/ScrollIntoViewOptions.kt
@@ -0,0 +1,7 @@
+package moe.nea89.website
+
+interface ScrollIntoViewOptions {
+ var behavior: String
+ var block: String
+ var inline: String
+} \ No newline at end of file
diff --git a/src/jsMain/kotlin/moe/nea89/website/ShellExecutionContext.kt b/src/jsMain/kotlin/moe/nea89/website/ShellExecutionContext.kt
new file mode 100644
index 0000000..bd72421
--- /dev/null
+++ b/src/jsMain/kotlin/moe/nea89/website/ShellExecutionContext.kt
@@ -0,0 +1,51 @@
+package moe.nea89.website
+
+import kotlinx.browser.window
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.*
+import kotlin.time.Duration
+import kotlin.time.DurationUnit
+
+class ShellExecutionContext(
+ val console: KConsole,
+ val name: String,
+ val args: List<String>,
+) {
+
+ suspend fun wait(duration: Duration) {
+ suspendCancellableCoroutine<Unit> {
+ window.setTimeout({
+ it.resume(Unit)
+ }, timeout = duration.toInt(DurationUnit.MILLISECONDS))
+ }
+ }
+
+ suspend fun exit(): Nothing {
+ suspendCancellableCoroutine<Unit> {
+ it.cancel()
+ console.state = KConsole.ConsoleState.SHELLPROMPT
+ console.rerender()
+ }
+ throw RuntimeException("THIs shOULDNT EXIST")
+ }
+
+ companion object {
+ fun run(
+ console: KConsole, command: Command, name: String, args: List<String>
+ ) {
+ console.state = KConsole.ConsoleState.IN_PROGRAM
+ val se = ShellExecutionContext(console, name, args)
+ window.requestAnimationFrame {
+ command.runner.createCoroutine(se, object : Continuation<Unit> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<Unit>) {
+ console.state = KConsole.ConsoleState.SHELLPROMPT
+ console.rerender()
+ }
+ }).resume(Unit)
+ }
+ }
+ }
+}
diff --git a/src/jsMain/kotlin/moe/nea89/website/Styles.kt b/src/jsMain/kotlin/moe/nea89/website/Styles.kt
new file mode 100644
index 0000000..e1470d7
--- /dev/null
+++ b/src/jsMain/kotlin/moe/nea89/website/Styles.kt
@@ -0,0 +1,55 @@
+package moe.nea89.website
+
+import kotlinx.css.*
+import kotlinx.css.properties.IterationCount
+import kotlinx.css.properties.Timing
+import kotlinx.css.properties.s
+import styled.StyleSheet
+import styled.animation
+
+
+object Styles : StyleSheet("DefaultConsoleStyles") {
+ val consoleClass = "Console"
+ val promptClass = "prompt"
+
+ val bgColor = CustomColor.BLACK.color
+ val fgColor = CustomColor.WHITE.color
+ val monospacedFont = "monospace"
+
+ val global by css {
+ "*" {
+ padding(0.px)
+ margin(0.px)
+ boxSizing = BoxSizing.borderBox
+ }
+
+ ".$promptClass" {
+ width = LinearDimension.fitContent
+ borderRightColor = fgColor
+ borderRightWidth = 2.px
+ paddingRight = 2.px
+ borderRightStyle = BorderStyle.solid
+ animation(1.s, Timing.stepStart, iterationCount = IterationCount.infinite) {
+ 0 {
+ borderRightStyle = BorderStyle.solid
+ }
+ 50 {
+ borderRightStyle = BorderStyle.none
+ }
+ }
+ }
+
+ ".$consoleClass" {
+ width = 100.pct
+ height = 100.pct
+ backgroundColor = bgColor
+ color = fgColor
+ fontFamily = monospacedFont
+ width = 100.pct
+ height = 100.pct
+ pre {
+ fontFamily = monospacedFont
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/jsMain/kotlin/moe/nea89/website/util.kt b/src/jsMain/kotlin/moe/nea89/website/util.kt
new file mode 100644
index 0000000..47c7843
--- /dev/null
+++ b/src/jsMain/kotlin/moe/nea89/website/util.kt
@@ -0,0 +1,6 @@
+package moe.nea89.website
+
+fun <T> dyn(init: T.() -> Unit): dynamic = js("{}").also(init)
+
+
+