diff options
author | nea <romangraef@gmail.com> | 2021-08-20 15:33:32 +0200 |
---|---|---|
committer | nea <romangraef@gmail.com> | 2021-08-20 15:33:58 +0200 |
commit | 5042498c8fe12751c1b6560ae1fde07e7cd78259 (patch) | |
tree | 688aa93978ffffd0b15c639768fdad6f17609bdd /src/jsMain/kotlin | |
parent | 65a6ffeb6761994d4d69011c87f23fe074f7a1f7 (diff) | |
download | webos-5042498c8fe12751c1b6560ae1fde07e7cd78259.tar.gz webos-5042498c8fe12751c1b6560ae1fde07e7cd78259.tar.bz2 webos-5042498c8fe12751c1b6560ae1fde07e7cd78259.zip |
Diffstat (limited to 'src/jsMain/kotlin')
-rw-r--r-- | src/jsMain/kotlin/WebOS.kt | 345 | ||||
-rw-r--r-- | src/jsMain/kotlin/io/Path.kt | 78 | ||||
-rw-r--r-- | src/jsMain/kotlin/io/files.kt | 203 | ||||
-rw-r--r-- | src/jsMain/kotlin/util/sequence.kt | 5 |
4 files changed, 0 insertions, 631 deletions
diff --git a/src/jsMain/kotlin/WebOS.kt b/src/jsMain/kotlin/WebOS.kt deleted file mode 100644 index 4ad2f86..0000000 --- a/src/jsMain/kotlin/WebOS.kt +++ /dev/null @@ -1,345 +0,0 @@ -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 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, -) diff --git a/src/jsMain/kotlin/io/Path.kt b/src/jsMain/kotlin/io/Path.kt deleted file mode 100644 index 241c559..0000000 --- a/src/jsMain/kotlin/io/Path.kt +++ /dev/null @@ -1,78 +0,0 @@ -package io - -sealed class Path { - abstract val parts: List<String> - fun toAbsolutePath(relativeTo: Absolute): Absolute { - return relativeTo.resolve(this) - } - - abstract fun resolve(path: Path): Path - - abstract val stringPath: String - - companion object { - val root = Absolute(listOf()) - - fun ofShell(string: String, userHome: Absolute): Path = - ofShell(string.split("/"), userHome) - - fun ofShell(vararg parts: String, userHome: Absolute): Path = - ofShell(parts.toList(), userHome) - - fun of(vararg parts: String): Path = - of(parts.toList()) - - fun of(string: String): Path = - of(string.split("/")) - - fun ofShell(parts: List<String>, userHome: Absolute): Path { - if (parts.firstOrNull() == "~") - return userHome.resolve(Relative(parts.subList(1, parts.size).filter { it.isNotEmpty() })) - return of(parts) - } - - fun of(parts: List<String>): Path { - if (parts.isEmpty()) - return root - if (parts[0] == "") // Starts with a / - return Absolute(parts.subList(1, parts.size).filter { it.isNotEmpty() }) - return Relative(parts.filter { it.isNotEmpty() }) - } - } - - data class Relative internal constructor(override val parts: List<String>) : Path() { - override fun resolve(path: Path): Path { - if (path is Absolute) return path - return Relative(this.parts + path.parts) - } - - override val stringPath: String get() = parts.joinToString("/") - } - - data class Absolute internal constructor(override val parts: List<String>) : Path() { - override fun resolve(path: Path): Absolute { - if (path is Absolute) return path - return Absolute(this.parts + path.parts) - } - - override val stringPath: String get() = "/" + parts.joinToString("/") - - fun relativize(path: Path): Relative = when (path) { - is Relative -> path - is Absolute -> { - var idx = 0 - while (idx < path.parts.size && idx < parts.size && path.parts[idx] == parts[idx]) { - idx++ - } - val returns = if (idx < parts.size) { - parts.size - idx - } else { - 0 - } - Relative(List(returns) { ".." } + path.parts.subList(idx, path.parts.size)) - } - } - } - - override fun toString(): String = "Path($stringPath)" -} diff --git a/src/jsMain/kotlin/io/files.kt b/src/jsMain/kotlin/io/files.kt deleted file mode 100644 index 4c53c4e..0000000 --- a/src/jsMain/kotlin/io/files.kt +++ /dev/null @@ -1,203 +0,0 @@ -package io - -import User - -sealed interface CreateFileResult { - object Created : CreateFileResult - sealed interface Failure : CreateFileResult { - object NoPermission : Failure - object NoParent : Failure - object AlreadyExists : Failure - } -} - -sealed interface WriteFileResult { - object Written : WriteFileResult - sealed interface Failure : WriteFileResult { - object NoPermission : Failure - object NotAFile : Failure - data class NotEnoughSpace( - val dataSize: Long, - val spaceLeft: Long - ) : Failure - } -} - -sealed interface ReadFileResult { - data class Read(val data: ByteArray) : ReadFileResult { - override fun equals(other: Any?): Boolean = (other as? Read)?.let { it.data.contentEquals(this.data) } ?: false - - override fun hashCode(): Int { - return data.contentHashCode() - } - } - - sealed interface Failure : ReadFileResult { - object NoPermission : Failure - object NotAFile : Failure - } -} - -sealed interface DeleteFileResult { - sealed interface Failure : DeleteFileResult { - object NoPermission : Failure - object NotAFile : Failure - } - - object Deleted : DeleteFileResult -} - -interface FileService<INode> { - fun getPath(iNode: INode): Path.Absolute - fun getINode(path: Path.Absolute): INode - fun createFile(iNode: INode, user: User): CreateFileResult - fun createSymlink(iNode: INode, user: User, path: Path): CreateFileResult - fun createDirectory(iNode: INode, user: User): CreateFileResult - fun writeToFile(iNode: INode, user: User, data: ByteArray): WriteFileResult - fun readFromFile(iNode: INode, user: User): ReadFileResult - fun deleteFile(iNode: INode, user: User): DeleteFileResult - fun exists(iNode: INode): Boolean - fun isFile(iNode: INode): Boolean - fun isDirectory(iNode: INode): Boolean - fun isSymlink(iNode: INode): Boolean - fun resolve(iNode: INode, fragment: String): INode - fun resolve(iNode: INode, path: Path.Relative): INode - fun changePermissions(iNode: INode, user: User, permissionUpdate: Map<User, Permission>) -} - -data class Permission( - val read: Boolean, - val write: Boolean, - val execute: Boolean -) { - companion object { - val default get() = Permission(read = true, write = true, execute = false) - } -} - -sealed interface PrimitiveStorageBlob { - val permissions: MutableMap<User, Permission> - - class File(var data: ByteArray, override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob - class Symlink(val path: Path, override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob - class Directory(override val permissions: MutableMap<User, Permission>) : PrimitiveStorageBlob -} - -data class PrimitiveINode internal constructor(internal val internalPath: String) -class PrimitiveFileService : FileService<PrimitiveINode> { - private val storageBlobs = mutableMapOf<String, PrimitiveStorageBlob>( - "/" to PrimitiveStorageBlob.Directory(mutableMapOf()) - ) - - override fun getPath(iNode: PrimitiveINode): Path.Absolute = Path.of(iNode.internalPath) as Path.Absolute - - override fun getINode(path: Path.Absolute): PrimitiveINode { - return resolve(PrimitiveINode("/"), Path.Relative(path.parts)) - } - - override fun resolve(iNode: PrimitiveINode, fragment: String): PrimitiveINode { - if (fragment == "..") { - val up = iNode.internalPath.substringBeforeLast('/') - if (up.isEmpty()) return PrimitiveINode("/") - return PrimitiveINode(up) - } - if (fragment.isEmpty() || fragment == ".") - return iNode - val blob = storageBlobs[iNode.internalPath] - return when (blob) { - is PrimitiveStorageBlob.Symlink -> { - when (blob.path) { - is Path.Absolute -> getINode(blob.path) - is Path.Relative -> resolve(resolve(iNode, ".."), blob.path) - } - } - else -> { - PrimitiveINode(iNode.internalPath + "/" + fragment) - } - } - } - - override fun resolve(iNode: PrimitiveINode, path: Path.Relative): PrimitiveINode = - path.parts.fold(iNode) { node, fragment -> resolve(node, fragment) } - - private fun getStorageBlob(iNode: PrimitiveINode): PrimitiveStorageBlob? = storageBlobs[iNode.internalPath] - - override fun writeToFile(iNode: PrimitiveINode, user: User, data: ByteArray): WriteFileResult { - val file = getStorageBlob(iNode) as? PrimitiveStorageBlob.File ?: return WriteFileResult.Failure.NotAFile - if (!hasPermission(user, file) { read }) - return WriteFileResult.Failure.NoPermission - file.data = data - return WriteFileResult.Written - } - - override fun readFromFile(iNode: PrimitiveINode, user: User): ReadFileResult { - val file = getStorageBlob(iNode) as? PrimitiveStorageBlob.File ?: return ReadFileResult.Failure.NotAFile - if (!hasPermission(user, file) { read }) - return ReadFileResult.Failure.NoPermission - return ReadFileResult.Read(file.data) - } - - override fun exists(iNode: PrimitiveINode): Boolean = - getStorageBlob(iNode) != null - - override fun isFile(iNode: PrimitiveINode): Boolean = - getStorageBlob(iNode) is PrimitiveStorageBlob.File - - override fun isDirectory(iNode: PrimitiveINode): Boolean = - getStorageBlob(iNode) is PrimitiveStorageBlob.Directory - - override fun isSymlink(iNode: PrimitiveINode): Boolean = - getStorageBlob(iNode) is PrimitiveStorageBlob.Symlink - - override fun changePermissions(iNode: PrimitiveINode, user: User, permissionUpdate: Map<User, Permission>) { - val file = getStorageBlob(iNode) ?: return // TODO Results - if (!hasPermission(user, file) { write }) - return // TODO Results - file.permissions.putAll(permissionUpdate) - return // TODO Results - } - - override fun deleteFile(iNode: PrimitiveINode, user: User): DeleteFileResult { - val file = getStorageBlob(iNode) ?: return DeleteFileResult.Failure.NotAFile - if (!hasPermission(user, file) { write }) - return DeleteFileResult.Failure.NoPermission - (storageBlobs.keys.filter { it.startsWith(iNode.internalPath + "/") } + listOf(iNode.internalPath)).forEach { - storageBlobs.remove(it) - } - return DeleteFileResult.Deleted - } - - private fun hasPermission(user: User, blob: PrimitiveStorageBlob, check: Permission.() -> Boolean): Boolean { - return user.isRoot || blob.permissions[user]?.let(check) ?: false - } - - private fun checkCreationPreconditions(iNode: PrimitiveINode, user: User): CreateFileResult? { - if (storageBlobs.containsKey(iNode.internalPath)) return CreateFileResult.Failure.AlreadyExists - val parent = getStorageBlob(resolve(iNode, "..")) - if (parent !is PrimitiveStorageBlob.Directory) return CreateFileResult.Failure.NoParent - if (!hasPermission(user, parent) { write }) return CreateFileResult.Failure.NoPermission - return null - } - - override fun createFile(iNode: PrimitiveINode, user: User): CreateFileResult { - val preconditions = checkCreationPreconditions(iNode, user) - if (preconditions != null) return preconditions - storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.File(byteArrayOf(), mutableMapOf(user to Permission.default)) - return CreateFileResult.Created - } - - override fun createSymlink(iNode: PrimitiveINode, user: User, path: Path): CreateFileResult { - val preconditions = checkCreationPreconditions(iNode, user) - if (preconditions != null) return preconditions - storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.Symlink(path, mutableMapOf(user to Permission.default)) - return CreateFileResult.Created - } - - override fun createDirectory(iNode: PrimitiveINode, user: User): CreateFileResult { - val preconditions = checkCreationPreconditions(iNode, user) - if (preconditions != null) return preconditions - storageBlobs[iNode.internalPath] = PrimitiveStorageBlob.Directory(mutableMapOf(user to Permission.default)) - return CreateFileResult.Created - } - -} diff --git a/src/jsMain/kotlin/util/sequence.kt b/src/jsMain/kotlin/util/sequence.kt deleted file mode 100644 index 72b07dc..0000000 --- a/src/jsMain/kotlin/util/sequence.kt +++ /dev/null @@ -1,5 +0,0 @@ -package util - -fun <T> Iterable<T>.expandWith(t: T): Sequence<T> = - this.asSequence() + generateSequence { t }.asSequence() - |