summaryrefslogtreecommitdiff
path: root/src/main/kotlin/WebOS.kt
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/WebOS.kt')
-rw-r--r--src/main/kotlin/WebOS.kt366
1 files changed, 366 insertions, 0 deletions
diff --git a/src/main/kotlin/WebOS.kt b/src/main/kotlin/WebOS.kt
new file mode 100644
index 0000000..5bb788c
--- /dev/null
+++ b/src/main/kotlin/WebOS.kt
@@ -0,0 +1,366 @@
+import io.FileService
+import io.Path
+import io.PrimitiveFileService
+import io.PrimitiveINode
+import kotlinx.browser.document
+import kotlinx.browser.window
+import org.w3c.dom.Element
+import org.w3c.dom.Node
+import org.w3c.dom.asList
+import org.w3c.dom.events.Event
+import org.w3c.dom.events.KeyboardEvent
+import kotlin.properties.Delegates
+
+fun main() {
+ console.log("Hello from Kotlin")
+
+ val runner = IORunner.runOnIO {
+ println("ON START")
+ wait()
+ println("WAIT CALLED ONCE")
+ val x = getOneKey()
+ println("INPUT GET: $x")
+ println("EXITING NOW")
+ }
+ println("runOnIO called -> CALLING WHATEVER")
+ runner._CALLED_BY_JS_onWhatever()
+ println("CALLED WHATEVER -> CALLING INPUT")
+ runner._CALLED_BY_JS_onInput(window.prompt("INPUT") ?: "NO INPUT")
+ println("CALLED INPUT")
+ return
+
+
+
+
+
+
+ val webos = WebOS()
+ document.body?.addEventListener("load", {
+ document.body?.querySelectorAll(".webosconsole")?.asList()?.forEach {
+ if (it !is Element) return@forEach
+ webos.registerConsole(it)
+ }
+ })
+}
+
+data class CharacterRun(val text: String, val color: String)
+
+abstract class Activity(val console: Console) {
+
+ open fun handleKeyPress(key: Key) {}
+ open fun update() {}
+ abstract fun render(columns: Int, rows: Int): List<List<CharacterRun>>
+}
+
+class ShellRunner(console: Console) : Activity(console) {
+
+ val history = mutableListOf<String>()
+
+ val shellProgramStack = ArrayDeque<ShellProgram>()
+
+ var lastLine = ""
+
+ fun newLine() {
+ history.add(lastLine)
+ lastLine = ""
+ console.invalidateRender()
+ }
+
+ fun println(it: String = "") {
+ print(it)
+ newLine()
+ }
+
+ fun print(it: String) {
+ lastLine += it
+ console.invalidateRender()
+ }
+
+ override fun update() {
+ shellProgramStack.lastOrNull()?.update()
+ }
+
+ fun getInput(): String? {
+ if (inputBuffer.contains("\n")) {
+ val r = inputBuffer.substringBefore("\n")
+ inputBuffer = inputBuffer.substringAfter("\n")
+ return r
+ }
+ return null
+ }
+
+ override fun render(columns: Int, rows: Int): List<List<CharacterRun>> =
+ history.map { listOf(CharacterRun(it.take(columns), "white")) }.takeLast(rows - 1) + listOf(
+ listOf(
+ CharacterRun(
+ lastLine + inputBuffer,
+ "white"
+ )
+ )
+ )
+
+ var inputBuffer = ""
+
+ override fun handleKeyPress(key: Key) = when (key) {
+ is Key.Printable -> {
+ inputBuffer += key.char
+ }
+ is Key.Enter -> {
+ inputBuffer += "\n"
+ }
+ else -> Unit
+ }
+
+ fun <T : ShellProgram> openShellProgram(program: (ShellRunner) -> T): ShellRunner = openShellProgram(program(this))
+
+ fun <T : ShellProgram> openShellProgram(program: T): ShellRunner {
+ shellProgramStack.addLast(program)
+ return this
+ }
+
+}
+
+abstract class ShellProgram(val shellRunner: ShellRunner) {
+
+ abstract fun update(): Unit
+}
+
+sealed class Key {
+ object Alt : Key()
+ object AltGraph : Key()
+ object CapsLock : Key()
+ object Control : Key()
+ object Function : Key()
+ object FunctionLock : Key()
+ object Hyper : Key()
+ object Meta : Key()
+ object NumLock : Key()
+ object Shift : Key()
+ object Super : Key()
+ object Symbol : Key()
+ object SymbolLock : Key()
+ object Enter : Key()
+ object Tab : Key()
+ sealed class Arrow : Key() {
+ object Down : Key()
+ object Left : Key()
+ object Right : Key()
+ object Up : Key()
+ }
+
+ object End : Key()
+ object Home : Key()
+ object PageUp : Key()
+ object PageDown : Key()
+ object Backspace : Key()
+ object Clear : Key()
+ object Copy : Key()
+ object CrSel : Key()
+ object Cut : Key()
+ object Delete : Key()
+ object EraseEof : Key()
+ object ExSel : Key()
+ object Insert : Key()
+ object Paste : Key()
+ object Redo : Key()
+ object Undo : Key()
+ object Accept : Key()
+ object Again : Key()
+ object Attn : Key()
+ object Cancel : Key()
+ object ContextMenu : Key()
+ object Escape : Key()
+ object Execute : Key()
+ object Find : Key()
+ object Finish : Key()
+ object Help : Key()
+ class Printable(val char: Char) : Key()
+ class FunctionN(val n: Int) : Key()
+ companion object {
+
+ fun from(string: String) = when (string) {
+ "Alt" -> Alt
+ "AltGraph" -> AltGraph
+ "CapsLock" -> CapsLock
+ "Control" -> Control
+ "Fn" -> Function
+ "FnLock" -> FunctionLock
+ "Hyper" -> Hyper
+ "Meta" -> Meta
+ "NumLock" -> NumLock
+ "Shift" -> Shift
+ "Super" -> Super
+ "Symbol" -> Symbol
+ "SymbolLock" -> SymbolLock
+ "Enter" -> Enter
+ "Tab" -> Tab
+ "Down" -> Arrow.Down
+ "Left" -> Arrow.Left
+ "Right" -> Arrow.Right
+ "Up" -> Arrow.Up
+ "End" -> End
+ "Home" -> Home
+ "PageUp" -> PageUp
+ "PageDown" -> PageDown
+ "Backspace" -> Backspace
+ "Clear" -> Clear
+ "Copy" -> Copy
+ "CrSel" -> CrSel
+ "Cut" -> Cut
+ "Delete" -> Delete
+ "EraseEof" -> EraseEof
+ "ExSel" -> ExSel
+ "Insert" -> Insert
+ "Paste" -> Paste
+ "Redo" -> Redo
+ "Undo" -> Undo
+ "Accept" -> Accept
+ "Again" -> Again
+ "Attn" -> Attn
+ "Cancel" -> Cancel
+ "ContextMenu" -> ContextMenu
+ "Escape" -> Escape
+ "Execute" -> Execute
+ "Find" -> Find
+ "Finish" -> Finish
+ "Help" -> Help
+ else -> if (string.length == 1)
+ Printable(string.first())
+ else if (string.first() == 'F')
+ FunctionN(string.substring(1).toInt())
+ else throw TODO()
+ }
+ }
+}
+
+class Login(shellRunner: ShellRunner) : ShellProgram(shellRunner) {
+
+ override fun update() {
+ while (true)
+ when (state) {
+ 0 -> {
+ shellRunner.print("Username: ")
+ state = 1
+ }
+ 1 -> {
+ val inp = shellRunner.getInput() ?: return
+ shellRunner.println(inp)
+ shellRunner.print("Password: ")
+ username = inp
+ state = 2
+ }
+ 2 -> {
+ val inp = shellRunner.getInput() ?: return
+ shellRunner.println(inp)
+ shellRunner.println("Login complete)")
+ password = inp
+ // TODO: check password and set user
+ state = 3
+ }
+ 3 -> {
+ TODO()
+ }
+ }
+
+ }
+
+ var state = 0
+ var username = ""
+ var password = ""
+
+}
+
+class Console(val os: WebOS, val renderElement: Element?) {
+
+ val isVirtual get() = renderElement == null
+ val activityStack = ArrayDeque<Activity>()
+
+ var columns: Int = 80
+ var rows: Int = 46
+
+ var shouldRerender = true
+
+ var currentUser: User? = null
+
+ private var _workingDirectory: Path.Absolute? = null
+
+ var workingDirectory: Path.Absolute
+ get() = _workingDirectory ?: currentUser?.homeDirectory ?: Path.root
+ set(value) {
+ _workingDirectory = value
+ }
+
+ init {
+ renderElement?.addEventListener("keydown", ::computeKeypressEvent)
+ }
+
+ fun computeKeypressEvent(event: Event) {
+ if (!isInFocus()) return
+ if (event !is KeyboardEvent) return
+ activityStack.lastOrNull()?.handleKeyPress(Key.from(event.key))
+ }
+
+ private fun isInFocus(): Boolean = renderElement.containsOrIs(document.activeElement)
+
+ private fun Node?.containsOrIs(node: Node?) = this == node || this?.contains(node) ?: false
+
+ fun <T : Activity> openActivity(activity: (Console) -> T): Console = openActivity(activity(this))
+
+ fun <T : Activity> openActivity(activity: T): Console {
+ activityStack.addLast(activity)
+ invalidateRender()
+ return this
+ }
+
+ fun render() {
+ if (renderElement == null) return
+ if (!shouldRerender) return
+ shouldRerender = false
+ val x = activityStack.lastOrNull()?.render(columns, rows)
+ console.log(x)
+ }
+
+ fun invalidateRender() {
+ shouldRerender = true
+ window.requestAnimationFrame { render() }
+ }
+
+ fun resize(newColumns: Int, newRows: Int) {
+ invalidateRender()
+ }
+
+ // TODO: Handle resizes of the renderElement
+
+ fun update(): Unit {
+ activityStack.lastOrNull()?.update()
+ }
+
+ var updateInterval by Delegates.notNull<Int>()
+
+ fun start(): Console {
+ updateInterval = window.setInterval(::update, 10)
+ return openActivity(ShellRunner(this).also { it.openShellProgram(Login(it)) })
+ }
+
+ fun destroy() {
+ window.clearInterval(updateInterval)
+ }
+
+}
+
+class WebOS {
+
+ val hostname: String = "host"
+ private val _consoles = mutableListOf<Console>()
+ val consoles get() = _consoles.toList()
+ val files: FileService<PrimitiveINode> = PrimitiveFileService()
+ fun registerConsole(element: Element) {
+ _consoles.add(Console(this, element))
+ }
+}
+
+data class User(
+ val name: String,
+ val homeDirectory: Path.Absolute,
+ val isRoot: Boolean = false,
+)