diff options
-rw-r--r-- | .github/workflows/test.yml | 38 | ||||
-rw-r--r-- | src/jsMain/kotlin/WebOS.kt | 82 | ||||
-rw-r--r-- | src/jsMain/kotlin/io/Path.kt | 75 | ||||
-rw-r--r-- | src/jsMain/kotlin/io/files.kt | 90 | ||||
-rw-r--r-- | src/jsMain/kotlin/util/sequence.kt | 5 | ||||
-rw-r--r-- | src/jsMain/resources/index.html | 17 | ||||
-rw-r--r-- | src/jsTest/kotlin/io/PathTest.kt | 32 |
7 files changed, 339 insertions, 0 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fe7d374 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +on: + - push + +name: Tests + +jobs: + test: + name: Run NodeJS Tests + steps: + - uses: actions/checkout@v2 + - name: Setup Java + with: + java-version: 16 + - name: Get Allure history + uses: actions/checkout@v2 + if: always() + continue-on-error: true + with: + ref: gh-pages + path: gh-pages + - name: Run Gradle Tests + run: ./gradlew jsNodeTest --stacktrace + - name: Generate Allure Report + if: always() + uses: simple-elf/allure-report-action@master + with: + allure_results: build/test-results/jsNodeTest + allure_history: allure-history + + + - name: Deploy report to Github Pages + if: always() + uses: peaceiris/actions-gh-pages@v2 + env: + PERSONAL_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PUBLISH_BRANCH: gh-pages + PUBLISH_DIR: allure-history + diff --git a/src/jsMain/kotlin/WebOS.kt b/src/jsMain/kotlin/WebOS.kt new file mode 100644 index 0000000..b7c4cfb --- /dev/null +++ b/src/jsMain/kotlin/WebOS.kt @@ -0,0 +1,82 @@ +import io.IOHandler +import io.Path +import kotlinx.browser.document +import kotlinx.browser.window +import org.w3c.dom.Element +import org.w3c.dom.asList + +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) { + abstract fun render(columns: Int, rows: Int): List<List<CharacterRun>> +} + +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 + } + + fun openActivity(activity: Activity) { + activityStack.addLast(activity) + invalidateRender() + } + + fun render() { + if (renderElement == null) return + if (!shouldRerender) return + shouldRerender = false + activityStack.last() + } + + fun invalidateRender() { + shouldRerender = true + window.requestAnimationFrame { render() } + } + + fun resize(newColumns: Int, newRows: Int) { + invalidateRender() + } + + // TODO: Handle resizes of the renderElement + +} + +class WebOS { + private val _consoles = mutableListOf<Console>() + val consoles get() = _consoles.toList() + val files = IOHandler() + fun registerConsole(element: Element) { + _consoles.add(Console(this, element)) + } +} + +data class User( + val name: String, + val homeDirectory: Path.Absolute +) + diff --git a/src/jsMain/kotlin/io/Path.kt b/src/jsMain/kotlin/io/Path.kt new file mode 100644 index 0000000..8f77203 --- /dev/null +++ b/src/jsMain/kotlin/io/Path.kt @@ -0,0 +1,75 @@ +package io + +sealed interface Path { + val parts: List<String> + fun toAbsolutePath(relativeTo: Absolute): Absolute { + return relativeTo.resolve(this) + } + + fun resolve(path: Path): Path + + 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) + } + } + + 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) + } + + fun relativize(path: Path): Relative = when (path) { + is Relative -> path + is Absolute -> { + var commonPrefix = true + val partList = mutableListOf<String>() + var returns = 0 + for ((idx, part) in path.parts.withIndex()) { + if (idx < this.parts.size) { + if (this.parts[idx] == part && commonPrefix) { + continue + } else { + commonPrefix = false + returns++ + } + } + partList.add(part) + } + Relative(List(returns) { "" } + partList) + } + } + } +} diff --git a/src/jsMain/kotlin/io/files.kt b/src/jsMain/kotlin/io/files.kt new file mode 100644 index 0000000..d37035d --- /dev/null +++ b/src/jsMain/kotlin/io/files.kt @@ -0,0 +1,90 @@ +package io + +import User + +class IOHandler { + val mounts = mutableListOf<Mount>() + fun mount(absolutePath: Path.Absolute, fileSystem: FileSystem) { + if (mounts.any { it.mountPoint == absolutePath }) + return // TODO sensible error message handling + mounts += Mount(absolutePath, fileSystem) + } + + fun unmount(mountPoint: Path.Absolute) { + mounts.removeAll { it.mountPoint == mountPoint } + } + + fun <T> findMountFor( + workingDirectory: Path.Absolute, + path: Path, + operation: FileSystem.(relativePath: Path) -> T + ): T { + val absolutPath = path.toAbsolutePath(workingDirectory) + val mount = mounts.filter { + it.mountPoint.parts.zip(absolutPath.parts).all { (a, b) -> a == b } + }.maxByOrNull { it.mountPoint.parts.size } ?: throw IllegalStateException("No mount present") + return mount.fileSystem.operation( + Path.Absolute( + absolutPath.parts.subList( + mount.mountPoint.parts.size, + absolutPath.parts.size + ) + ) // TODO: unangenehm + ) + } + + fun findINode(absolutePath: Path.Absolute): INode { + val mount = mounts.filter { + it.mountPoint.parts.zip(absolutePath.parts).all { (a, b) -> a == b } + }.maxByOrNull { it.mountPoint.parts.size } ?: throw IllegalStateException("No mount present") + val iNode = mount.fileSystem.getINode(absolutePath.relativize(mount.mountPoint)) + return when (iNode) { + is INodeResult.File -> iNode.op + is INodeResult.ResolveAgain -> findINode(absolutePath.resolve(iNode.relativeToOriginal)) + } + } + + fun read(workingDirectory: Path.Absolute, path: Path): ReadResult = + findMountFor(workingDirectory, path) { read(it) } + + fun write(workingDirectory: Path.Absolute, path: Path, data: ByteArray): Unit = + findMountFor(workingDirectory, path) { write(it, data) } + + fun stat(workingDirectory: Path.Absolute, path: Path): Unit = + findMountFor(workingDirectory, path) { stat(it) } +} + +interface INode { + val fs: FileSystem +} + +sealed interface INodeResult { + class File(val op: INode) : INodeResult + class ResolveAgain(val relativeToOriginal: Path): INodeResult +} + +interface FileSystem { + fun getINode(relativePath: Path.Relative): INodeResult + fun read(relativePath: Path): ReadResult + fun write(path: Path, data: ByteArray): Unit // Write result + fun stat(path: Path): Unit // TODO io.Stat result +} + +sealed class ReadResult { + class Success(val text: String) : ReadResult() + object NotFound : ReadResult() + object NoAccess : ReadResult() +} + +data class Mount( + val mountPoint: Path.Absolute, + val fileSystem: FileSystem +) + +data class Stat( + val exists: Boolean, + var owner: User, + val created: Long, + val edited: Long, + val size: Long +) diff --git a/src/jsMain/kotlin/util/sequence.kt b/src/jsMain/kotlin/util/sequence.kt new file mode 100644 index 0000000..72b07dc --- /dev/null +++ b/src/jsMain/kotlin/util/sequence.kt @@ -0,0 +1,5 @@ +package util + +fun <T> Iterable<T>.expandWith(t: T): Sequence<T> = + this.asSequence() + generateSequence { t }.asSequence() + diff --git a/src/jsMain/resources/index.html b/src/jsMain/resources/index.html new file mode 100644 index 0000000..719506b --- /dev/null +++ b/src/jsMain/resources/index.html @@ -0,0 +1,17 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" + content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <title>WebOs</title> +</head> +<body> +<noscript>tf you don't have js enabled? do you still live in the stone ages or what?</noscript> +<div id="content"> + +</div> +<script src="/webos.js"></script> +</body> +</html> diff --git a/src/jsTest/kotlin/io/PathTest.kt b/src/jsTest/kotlin/io/PathTest.kt new file mode 100644 index 0000000..2649a2a --- /dev/null +++ b/src/jsTest/kotlin/io/PathTest.kt @@ -0,0 +1,32 @@ +package io + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.types.shouldBeTypeOf + +class PathTest : DescribeSpec({ + describe("Path") { + val homeDir = Path.of("/home") as Path.Absolute + it("recognize relative paths as such") { + listOf( + Path.of("a/b"), + Path.of("."), + Path.of("a", "b"), + Path.ofShell("a/b", userHome = homeDir), + Path.ofShell(".", userHome = homeDir), + Path.ofShell("a", "b", userHome = homeDir), + Path.ofShell(listOf("a", "b"), userHome = homeDir), + ).forEach { + it.shouldBeTypeOf<Path.Relative>() + } + } + it("recognize absolute paths as such") { + listOf( + Path.of("/a/b"), + Path.of("/"), + Path.ofShell("/b/c", userHome = homeDir), + ).forEach { + it.shouldBeTypeOf<Path.Absolute>() + } + } + } +}) |