diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 911415ca8..14046d76a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,22 +17,26 @@ jobs: test: strategy: matrix: - os: [macos-latest, macos-14, windows-latest, ubuntu-latest] + os: [macos-latest, windows-latest, ubuntu-latest] include: - os: ubuntu-latest EXTRA_GRADLE_ARGS: :test:graalvm:nativeTest apiCheck + - os: macos-latest + env: + # macos-latest is now macos-14 and has less than half as much memory available as other runners + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8" -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v4 - uses: graalvm/setup-graalvm@v1 with: - java-version: 17 + java-version: 21 distribution: 'graalvm-community' set-java-home: false - uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: 17 + java-version: 21 - uses: gradle/gradle-build-action@v2 with: arguments: | @@ -74,4 +78,4 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKey }} ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralUsername }} env: - GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true -Dorg.gradle.jvmargs="-Dfile.encoding=UTF-8" + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx12g -Dfile.encoding=UTF-8" -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index a9d595224..394c3db4d 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -6,10 +6,11 @@ on: - 'gradlew.bat' - 'gradle/wrapper/' + jobs: validation: name: "Validation" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/actions/wrapper-validation@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c654053d..f220aaa81 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: 17 + java-version: 21 - uses: actions/setup-python@v5 with: python-version: '3.12' @@ -43,3 +43,6 @@ jobs: with: branch: gh-pages folder: site +env: + # macos-latest is now macos-14 and has less than half as much memory available as other runners + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8" -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true diff --git a/.gitignore b/.gitignore index 75734f44a..f6ac8b49d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ docs/changelog.md docs/index.md site/ kotlin-js-store/ +.kotlin/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b76a1cd..efd3cd15b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## Unreleased +### Added +- Publish `linuxArm64` and `wasmJs` targets. + ## 2.4.0 This release includes a complete rewrite of the progress bar system. The new system is more performant and flexible, and allows for more complex progress animations. The old progress bar APIs diff --git a/build.gradle.kts b/build.gradle.kts index 9b0aca00f..61669968b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,6 @@ import org.jetbrains.dokka.gradle.DokkaMultiModuleTask +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension +import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask plugins { alias(libs.plugins.kotlinBinaryCompatibilityValidator) @@ -21,3 +23,8 @@ tasks.withType().configureEach { ) ) } + +// https://youtrack.jetbrains.com/issue/KT-63014 +tasks.withType().configureEach { + args.add("--ignore-engines") +} diff --git a/buildSrc/src/main/kotlin/mordant-js-conventions.gradle.kts b/buildSrc/src/main/kotlin/mordant-js-conventions.gradle.kts index a2b661bce..af2088fb7 100644 --- a/buildSrc/src/main/kotlin/mordant-js-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/mordant-js-conventions.gradle.kts @@ -1,3 +1,6 @@ +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension + plugins { kotlin("multiplatform") } @@ -8,4 +11,22 @@ kotlin { nodejs() browser() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + nodejs() + browser() + } + + sourceSets { + val jsCommonMain by creating { dependsOn(commonMain.get()) } + jsMain.get().dependsOn(jsCommonMain) + getByName("wasmJsMain").dependsOn(jsCommonMain) + } +} + +// Need to compile using a canary version of Node due to +// https://youtrack.jetbrains.com/issue/KT-63014 +rootProject.the().apply { + nodeVersion = "21.0.0-v8-canary2023091837d0630120" + nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary" } diff --git a/buildSrc/src/main/kotlin/mordant-native-conventions.gradle.kts b/buildSrc/src/main/kotlin/mordant-native-conventions.gradle.kts index 787e6ae7a..288e14d82 100644 --- a/buildSrc/src/main/kotlin/mordant-native-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/mordant-native-conventions.gradle.kts @@ -6,6 +6,7 @@ kotlin { macosX64() macosArm64() linuxX64() + linuxArm64() mingwX64() applyDefaultHierarchyTemplate() diff --git a/extensions/mordant-coroutines/src/commonTest/kotlin/com/github/ajalt/mordant/animation/coroutines/CoroutinesAnimatorTest.kt b/extensions/mordant-coroutines/src/commonTest/kotlin/com/github/ajalt/mordant/animation/coroutines/CoroutinesAnimatorTest.kt index e1a736acb..5c15a580b 100644 --- a/extensions/mordant-coroutines/src/commonTest/kotlin/com/github/ajalt/mordant/animation/coroutines/CoroutinesAnimatorTest.kt +++ b/extensions/mordant-coroutines/src/commonTest/kotlin/com/github/ajalt/mordant/animation/coroutines/CoroutinesAnimatorTest.kt @@ -75,7 +75,7 @@ class CoroutinesAnimatorTest { a.stop() advanceTimeBy(1.0.seconds) job.isActive shouldBe false - vt.output() shouldBe "$HIDE_CURSOR 0/10\n$SHOW_CURSOR" + vt.fullNormalizedOutput() shouldBe "$HIDE_CURSOR 0/10\n$SHOW_CURSOR" vt.clearOutput() job = backgroundScope.launch { a.execute() } @@ -83,7 +83,7 @@ class CoroutinesAnimatorTest { a.clear() advanceTimeBy(1.0.seconds) job.isActive shouldBe false - vt.output() shouldBe "$HIDE_CURSOR 0/10\r${CSI}0J$SHOW_CURSOR" + vt.fullNormalizedOutput() shouldBe "$HIDE_CURSOR 0/10\r${CSI}0J$SHOW_CURSOR" } @Test @@ -125,7 +125,12 @@ class CoroutinesAnimatorTest { vt.output().shouldContain(" 10/10\n 10/10") } + // This handles the difference in wasm movements and the other targets + private fun TerminalRecorder.fullNormalizedOutput(): String { + return output().replace("${CSI}1A", "") + } + private fun TerminalRecorder.normalizedOutput(): String { - return output().substringAfter("\r").trimEnd() + return output().replace("${CSI}1A", "").substringAfter("\r").trimEnd() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index efc7242f5..5cbf090e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] -kotlin = "1.9.21" +kotlin = "1.9.23" coroutines = "1.8.0" [libraries] -colormath = "com.github.ajalt.colormath:colormath:3.3.1" -markdown = "org.jetbrains:markdown:0.5.2" -jna-core = "net.java.dev.jna:jna:5.13.0" +colormath = "com.github.ajalt.colormath:colormath:3.5.0" +markdown = "org.jetbrains:markdown:0.7.0" +jna-core = "net.java.dev.jna:jna:5.14.0" # compileOnly graalvm-svm = "org.graalvm.nativeimage:svm:23.1.0" @@ -14,15 +14,15 @@ graalvm-svm = "org.graalvm.nativeimage:svm:23.1.0" coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } # used in tests -kotest = "io.kotest:kotest-assertions-core:5.8.0" +kotest = "io.kotest:kotest-assertions-core:5.9.0.1440-SNAPSHOT" systemrules = "com.github.stefanbirkner:system-rules:1.19.0" r8 = "com.android.tools:r8:8.3.37" coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } # build logic kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.9.10" } -publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.27.0" } +dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.9.20" } +publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.28.0" } [plugins] graalvm-nativeimage = "org.graalvm.buildtools.native:0.9.28" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917..e6441136f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e0930..b82aa23a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 6689b85be..7101f8e46 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/mordant/api/mordant.api b/mordant/api/mordant.api index 7ef5557ef..8e22afd45 100644 --- a/mordant/api/mordant.api +++ b/mordant/api/mordant.api @@ -263,6 +263,10 @@ public final class com/github/ajalt/mordant/rendering/Line : java/util/List, kot public synthetic fun add (Ljava/lang/Object;)Z public fun addAll (ILjava/util/Collection;)Z public fun addAll (Ljava/util/Collection;)Z + public fun addFirst (Lcom/github/ajalt/mordant/rendering/Span;)V + public synthetic fun addFirst (Ljava/lang/Object;)V + public fun addLast (Lcom/github/ajalt/mordant/rendering/Span;)V + public synthetic fun addLast (Ljava/lang/Object;)V public fun clear ()V public final fun component1 ()Ljava/util/List; public final fun component2 ()Lcom/github/ajalt/mordant/rendering/TextStyle; @@ -290,6 +294,10 @@ public final class com/github/ajalt/mordant/rendering/Line : java/util/List, kot public synthetic fun remove (I)Ljava/lang/Object; public fun remove (Ljava/lang/Object;)Z public fun removeAll (Ljava/util/Collection;)Z + public fun removeFirst ()Lcom/github/ajalt/mordant/rendering/Span; + public synthetic fun removeFirst ()Ljava/lang/Object; + public fun removeLast ()Lcom/github/ajalt/mordant/rendering/Span; + public synthetic fun removeLast ()Ljava/lang/Object; public fun replaceAll (Ljava/util/function/UnaryOperator;)V public fun retainAll (Ljava/util/Collection;)Z public fun set (ILcom/github/ajalt/mordant/rendering/Span;)Lcom/github/ajalt/mordant/rendering/Span; @@ -366,6 +374,7 @@ public final class com/github/ajalt/mordant/rendering/TextColors : java/lang/Enu public static final field red Lcom/github/ajalt/mordant/rendering/TextColors; public static final field white Lcom/github/ajalt/mordant/rendering/TextColors; public static final field yellow Lcom/github/ajalt/mordant/rendering/TextColors; + public fun clamp ()Lcom/github/ajalt/colormath/Color; public fun getAlpha ()F public fun getBg ()Lcom/github/ajalt/mordant/rendering/TextStyle; public fun getBgColor ()Lcom/github/ajalt/colormath/Color; diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/animation/Animation.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/animation/Animation.kt index c788f8918..6a7cc0e5b 100644 --- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/animation/Animation.kt +++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/animation/Animation.kt @@ -1,5 +1,6 @@ package com.github.ajalt.mordant.animation +import com.github.ajalt.mordant.internal.* import com.github.ajalt.mordant.internal.FAST_ISATTY import com.github.ajalt.mordant.internal.MppAtomicRef import com.github.ajalt.mordant.internal.Size @@ -173,6 +174,7 @@ abstract class Animation( if (firstDraw || lastSize == null) return null return terminal.cursor.getMoves { startOfLine() + if (CR_IMPLIES_LF) up(1) if (terminal.info.crClearsLine) { // IntelliJ doesn't support cursor moves, so this is all we can do diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppH.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppH.kt index 5eb1a30ed..61f10012e 100644 --- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppH.kt +++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppH.kt @@ -54,3 +54,5 @@ internal expect fun sendInterceptedPrintRequest( ) internal expect val FAST_ISATTY: Boolean + +internal expect val CR_IMPLIES_LF: Boolean diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Lines.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Lines.kt index e3195f29c..ae7db3ea7 100644 --- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Lines.kt +++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Lines.kt @@ -156,7 +156,7 @@ private fun resizeLine( endIndex = j if (width < newWidth) { endSpan = span.take(newWidth - width) - width += endSpan.cellWidth + width = newWidth } break } diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/animation/AnimationTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/animation/AnimationTest.kt index 836280697..6fd235426 100644 --- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/animation/AnimationTest.kt +++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/animation/AnimationTest.kt @@ -1,8 +1,10 @@ package com.github.ajalt.mordant.animation +import com.github.ajalt.mordant.internal.CSI import com.github.ajalt.mordant.terminal.Terminal import com.github.ajalt.mordant.terminal.TerminalRecorder -import com.github.ajalt.mordant.test.replaceCrLf +import com.github.ajalt.mordant.test.normalizedOutput +import com.github.ajalt.mordant.test.visibleCrLf import io.kotest.matchers.shouldBe import kotlin.js.JsName import kotlin.test.Test @@ -16,13 +18,13 @@ class AnimationTest { fun `no trailing linebreak`() { val a = t.textAnimation(trailingLinebreak = false) { "<$it>\n===" } a.update(1) - rec.output() shouldBe "<1>\n===" + rec.normalizedOutput() shouldBe "<1>\n===" // update rec.clearOutput() a.update(2) val moves = t.cursor.getMoves { startOfLine(); up(1) } - rec.output() shouldBe "${moves}<2>\n===" + rec.normalizedOutput() shouldBe "${moves}<2>\n===" } @Test @@ -30,13 +32,13 @@ class AnimationTest { fun `no trailing linebreak single line`() { val a = t.textAnimation(trailingLinebreak = false) { "<$it>" } a.update(1) - rec.output() shouldBe "<1>" + rec.normalizedOutput() shouldBe "<1>" // update rec.clearOutput() a.update(2) val moves = t.cursor.getMoves { startOfLine() } - rec.output() shouldBe "${moves}<2>" + rec.normalizedOutput() shouldBe "${moves}<2>" } @Test @@ -44,13 +46,13 @@ class AnimationTest { fun `animation size change`() { val a = t.textAnimation { it } a.update("1") - rec.output() shouldBe "1" + rec.normalizedOutput() shouldBe "1" // update rec.clearOutput() a.update("2\n3") val moves = t.cursor.getMoves { startOfLine() } - rec.output() shouldBe "${moves}2\n3" + rec.normalizedOutput() shouldBe "${moves}2\n3" } @Test @@ -59,11 +61,11 @@ class AnimationTest { val t = Terminal(terminalInterface = rec) val a = t.textAnimation { "$it" } a.update(1) - rec.output() shouldBe "1" + rec.normalizedOutput() shouldBe "1" a.update(2) - rec.output() shouldBe "1\r2" + rec.normalizedOutput() shouldBe "1\r2" a.stop() - rec.output() shouldBe "1\r2\n" + rec.normalizedOutput() shouldBe "1\r2\n" } @Test @@ -71,50 +73,50 @@ class AnimationTest { fun `print during animation`() { val a = t.textAnimation { "<$it>\n===" } a.update(1) - rec.output() shouldBe "<1>\n===" + rec.normalizedOutput() shouldBe "<1>\n===" // update rec.clearOutput() a.update(2) var moves = t.cursor.getMoves { startOfLine(); up(1) } - rec.output() shouldBe "${moves}<2>\n===" + rec.normalizedOutput() shouldBe "${moves}<2>\n===" // print while active rec.clearOutput() t.println("X") moves = t.cursor.getMoves { startOfLine(); up(1); clearScreenAfterCursor() } - rec.output() shouldBe "${moves}X\n<2>\n===" + rec.normalizedOutput() shouldBe "${moves}X\n<2>\n===" // clear rec.clearOutput() a.clear() moves = t.cursor.getMoves { startOfLine(); up(1); clearScreenAfterCursor() } - rec.output() shouldBe moves + rec.normalizedOutput() shouldBe moves // repeat clear rec.clearOutput() a.clear() - rec.output() shouldBe "" + rec.normalizedOutput() shouldBe "" // update after clear rec.clearOutput() a.update(3) - rec.output() shouldBe "<3>\n===" + rec.normalizedOutput() shouldBe "<3>\n===" // stop rec.clearOutput() a.stop() - rec.output() shouldBe "\n" + rec.normalizedOutput() shouldBe "\n" // print after stop rec.clearOutput() t.println("X") - rec.output() shouldBe "X\n" + rec.normalizedOutput() shouldBe "X\n" // update after stop rec.clearOutput() a.update(4) - rec.output() shouldBe "<4>\n===" + rec.normalizedOutput() shouldBe "<4>\n===" } @Test @@ -124,27 +126,26 @@ class AnimationTest { val b = t.textAnimation { "" } a.update(1) - rec.output().replaceCrLf() shouldBe "" + rec.normalizedOutput().visibleCrLf() shouldBe "" rec.clearOutput() b.update(2) var moves = t.cursor.getMoves { startOfLine() } - rec.output().replaceCrLf() shouldBe "${moves}\n".replaceCrLf() + rec.normalizedOutput().visibleCrLf() shouldBe "${moves}\n".visibleCrLf() rec.clearOutput() b.update(3) moves = t.cursor.getMoves { startOfLine(); up(1); clearScreenAfterCursor(); startOfLine() } - rec.output().replaceCrLf() shouldBe "${moves}\n".replaceCrLf() + rec.normalizedOutput().visibleCrLf() shouldBe "${moves}\n".visibleCrLf() rec.clearOutput() a.stop() - rec.output().replaceCrLf() shouldBe "\r".replaceCrLf() + rec.normalizedOutput().visibleCrLf() shouldBe "\r".visibleCrLf() rec.clearOutput() b.stop() - rec.output().replaceCrLf() shouldBe "\n".replaceCrLf() + rec.normalizedOutput().visibleCrLf() shouldBe "\n".visibleCrLf() } - } diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/animation/progress/BaseProgressAnimationTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/animation/progress/BaseProgressAnimationTest.kt index 1fa6d02ae..d745f294f 100644 --- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/animation/progress/BaseProgressAnimationTest.kt +++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/animation/progress/BaseProgressAnimationTest.kt @@ -5,8 +5,9 @@ import com.github.ajalt.mordant.rendering.Theme import com.github.ajalt.mordant.terminal.Terminal import com.github.ajalt.mordant.terminal.TerminalRecorder import com.github.ajalt.mordant.test.RenderingTest +import com.github.ajalt.mordant.test.latestOutput import com.github.ajalt.mordant.test.normalizedOutput -import com.github.ajalt.mordant.test.replaceCrLf +import com.github.ajalt.mordant.test.visibleCrLf import com.github.ajalt.mordant.widgets.progress.* import io.kotest.matchers.shouldBe import kotlin.js.JsName @@ -34,28 +35,28 @@ class BaseProgressAnimationTest : RenderingTest() { val pt = a.addTask(l, total = 1000) a.refresh() - vt.normalizedOutput() shouldBe " ---.-/s|eta -:--:--" + vt.latestOutput() shouldBe " ---.-/s|eta -:--:--" now += 0.5.seconds vt.clearOutput() pt.update(40) a.refresh() - vt.normalizedOutput() shouldBe " ---.-/s|eta -:--:--" + vt.latestOutput() shouldBe " ---.-/s|eta -:--:--" now += 0.1.seconds vt.clearOutput() a.refresh() - vt.normalizedOutput() shouldBe " ---.-/s|eta -:--:--" + vt.latestOutput() shouldBe " ---.-/s|eta -:--:--" now += 0.4.seconds vt.clearOutput() a.refresh() - vt.normalizedOutput() shouldBe " 40.0/s|eta 0:00:24" + vt.latestOutput() shouldBe " 40.0/s|eta 0:00:24" now += 0.9.seconds vt.clearOutput() a.refresh() - vt.normalizedOutput() shouldBe " 40.0/s|eta 0:00:24" + vt.latestOutput() shouldBe " 40.0/s|eta 0:00:24" } @Test @@ -77,29 +78,29 @@ class BaseProgressAnimationTest : RenderingTest() { val pt = a.addTask(l, total = 100) a.refresh() - vt.normalizedOutput() shouldBe "text.txt| 0%|......| 0/100| ---.-/s|eta -:--:--" + vt.latestOutput() shouldBe "text.txt| 0%|......| 0/100| ---.-/s|eta -:--:--" now += 10.0.seconds vt.clearOutput() pt.update(40) a.refresh() - vt.normalizedOutput() shouldBe "text.txt| 40%|##>...| 40/100| 4.0/s|eta 0:00:15" + vt.latestOutput() shouldBe "text.txt| 40%|##>...| 40/100| 4.0/s|eta 0:00:15" now += 10.0.seconds vt.clearOutput() a.refresh() - vt.normalizedOutput() shouldBe "text.txt| 40%|##>...| 40/100| 2.0/s|eta 0:00:30" + vt.latestOutput() shouldBe "text.txt| 40%|##>...| 40/100| 2.0/s|eta 0:00:30" now += 10.0.seconds vt.clearOutput() pt.update { total = 200 } a.refresh() - vt.normalizedOutput() shouldBe "text.txt| 20%|#>....| 40/200| 1.3/s|eta 0:02:00" + vt.latestOutput() shouldBe "text.txt| 20%|#>....| 40/200| 1.3/s|eta 0:02:00" vt.clearOutput() pt.reset() a.refresh() - vt.normalizedOutput() shouldBe "text.txt| 0%|......| 0/200| ---.-/s|eta -:--:--" + vt.latestOutput() shouldBe "text.txt| 0%|......| 0/200| ---.-/s|eta -:--:--" } @Test @@ -113,27 +114,27 @@ class BaseProgressAnimationTest : RenderingTest() { val t2 = a.addTask(l, 2) a.refresh() - vt.normalizedOutput() shouldBe "Task 1\nTask 2" + vt.latestOutput() shouldBe "Task 1\nTask 2" vt.clearOutput() t1.update { visible = false } a.refresh() - vt.normalizedOutput() shouldBe "Task 2" + vt.latestOutput() shouldBe "Task 2" vt.clearOutput() t2.update { visible = false } a.refresh() - vt.normalizedOutput() shouldBe "" + vt.latestOutput() shouldBe "" vt.clearOutput() t1.update { visible = true } a.refresh() - vt.normalizedOutput() shouldBe "Task 1" + vt.latestOutput() shouldBe "Task 1" vt.clearOutput() a.removeTask(t1) a.refresh() - vt.normalizedOutput() shouldBe "" + vt.latestOutput() shouldBe "" } @Test @@ -153,7 +154,7 @@ class BaseProgressAnimationTest : RenderingTest() { now += 1.seconds a.refresh() val moves = t.cursor.getMoves { startOfLine(); up(1) } - vt.output().replaceCrLf() shouldBe "====\n1111${moves}====\n22 ".replaceCrLf() + vt.normalizedOutput().visibleCrLf() shouldBe "====\n1111${moves}====\n22 ".visibleCrLf() } @Test diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/TestUtils.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/TestUtils.kt index 1da61f7cc..39085d587 100644 --- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/TestUtils.kt +++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/TestUtils.kt @@ -1,5 +1,6 @@ package com.github.ajalt.mordant.test +import com.github.ajalt.mordant.internal.CR_IMPLIES_LF import com.github.ajalt.mordant.internal.CSI import com.github.ajalt.mordant.terminal.TerminalRecorder @@ -11,10 +12,14 @@ fun String.normalizeHyperlinks(): String { return regex.replace(this) { ";id=${map[it.value]};" } } -fun String.replaceCrLf(): String { +fun String.visibleCrLf(): String { return replace("\r", "␍").replace("\n", "␊").replace(CSI, "␛") } +// This handles the difference in wasm movements and the other targets fun TerminalRecorder.normalizedOutput(): String { - return output().substringAfter("${CSI}0J").substringAfter('\r') + return if (CR_IMPLIES_LF) output().replace("\r${CSI}1A", "\r") else output() +} +fun TerminalRecorder.latestOutput(): String { + return normalizedOutput().substringAfter("${CSI}0J").substringAfter('\r') } diff --git a/mordant/src/jsCommonMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt b/mordant/src/jsCommonMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt new file mode 100644 index 000000000..2f26e3312 --- /dev/null +++ b/mordant/src/jsCommonMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt @@ -0,0 +1,128 @@ +package com.github.ajalt.mordant.internal + +import com.github.ajalt.mordant.terminal.* + + +// Since `js()` and `external` work differently in wasm and js, we need to define the functions that +// use them twice +internal expect fun makeNodeMppImpls(): JsMppImpls? +internal expect fun browserPrintln(message: String) + +private class JsAtomicRef(override var value: T) : MppAtomicRef { + override fun compareAndSet(expected: T, newValue: T): Boolean { + if (value != expected) return false + value = newValue + return true + } + + override fun getAndSet(newValue: T): T { + val old = value + value = newValue + return old + } +} + +private class JsAtomicInt(initial: Int) : MppAtomicInt { + private var backing = initial + override fun getAndIncrement(): Int { + return backing++ + } + + override fun get(): Int { + return backing + } + + override fun set(value: Int) { + backing = value + } +} + +internal actual fun MppAtomicInt(initial: Int): MppAtomicInt = JsAtomicInt(initial) +internal actual fun MppAtomicRef(value: T): MppAtomicRef = JsAtomicRef(value) + + +internal interface JsMppImpls { + fun readEnvvar(key: String): String? + fun stdoutInteractive(): Boolean + fun stdinInteractive(): Boolean + fun stderrInteractive(): Boolean + fun getTerminalSize(): Size? + fun printStderr(message: String, newline: Boolean) + fun readLineOrNull(): String? + fun makeTerminalCursor(terminal: Terminal): TerminalCursor +} + +private object BrowserMppImpls : JsMppImpls { + override fun readEnvvar(key: String): String? = null + override fun stdoutInteractive(): Boolean = false + override fun stdinInteractive(): Boolean = false + override fun stderrInteractive(): Boolean = false + override fun getTerminalSize(): Size? = null + override fun printStderr(message: String, newline: Boolean) = browserPrintln(message) + + // readlnOrNull will just throw an exception on browsers + override fun readLineOrNull(): String? = readlnOrNull() + override fun makeTerminalCursor(terminal: Terminal): TerminalCursor { + return BrowserTerminalCursor(terminal) + } +} + +internal abstract class BaseNodeMppImpls : JsMppImpls { + final override fun readLineOrNull(): String? { + return try { + buildString { + val buf = allocBuffer(1) + do { + val len = readSync( + fd = 0, buffer = buf, offset = 0, len = 1 + ) + if (len == 0) break + val char = "$buf" // don't call toString here due to KT-55817 + append(char) + } while (char != "\n" && char != "${0.toChar()}") + } + } catch (e: Exception) { + null + } + } + + abstract fun allocBuffer(size: Int): BufferT + abstract fun readSync(fd: Int, buffer: BufferT, offset: Int, len: Int): Int + +} + +private val impls: JsMppImpls = makeNodeMppImpls() ?: BrowserMppImpls + +internal actual fun runningInIdeaJavaAgent(): Boolean = false + +internal actual fun getTerminalSize(): Size? = impls.getTerminalSize() +internal actual fun getEnv(key: String): String? = impls.readEnvvar(key) +internal actual fun stdoutInteractive(): Boolean = impls.stdoutInteractive() +internal actual fun stdinInteractive(): Boolean = impls.stdinInteractive() +internal actual fun printStderr(message: String, newline: Boolean) { + impls.printStderr(message, newline) +} + +// hideInput is not currently implemented +internal actual fun readLineOrNullMpp(hideInput: Boolean): String? = impls.readLineOrNull() + + +internal actual fun makePrintingTerminalCursor(terminal: Terminal): TerminalCursor { + return impls.makeTerminalCursor(terminal) +} + +// There are no shutdown hooks on browsers, so we don't need to do anything here +private class BrowserTerminalCursor(terminal: Terminal) : PrintTerminalCursor(terminal) + + +internal actual fun sendInterceptedPrintRequest( + request: PrintRequest, + terminalInterface: TerminalInterface, + interceptors: List, +) { + terminalInterface.completePrintRequest( + interceptors.fold(request) { acc, it -> it.intercept(acc) } + ) +} + +internal actual val FAST_ISATTY: Boolean = true diff --git a/mordant/src/jsMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt b/mordant/src/jsMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt index b543dde4b..f55c992ff 100644 --- a/mordant/src/jsMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt +++ b/mordant/src/jsMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt @@ -1,75 +1,20 @@ package com.github.ajalt.mordant.internal -import com.github.ajalt.mordant.terminal.* +import com.github.ajalt.mordant.terminal.PrintTerminalCursor +import com.github.ajalt.mordant.terminal.Terminal +import com.github.ajalt.mordant.terminal.TerminalCursor private external val process: dynamic private external val console: dynamic private external val Symbol: dynamic private external val Buffer: dynamic -private class JsAtomicRef(override var value: T) : MppAtomicRef { - override fun compareAndSet(expected: T, newValue: T): Boolean { - if (value != expected) return false - value = newValue - return true - } - - override fun getAndSet(newValue: T): T { - val old = value - value = newValue - return old - } -} - -private class JsAtomicInt(initial: Int) : MppAtomicInt { - private var backing = initial - override fun getAndIncrement(): Int { - return backing++ - } - - override fun get(): Int { - return backing - } - - override fun set(value: Int) { - backing = value - } -} - -internal actual fun MppAtomicInt(initial: Int): MppAtomicInt = JsAtomicInt(initial) -internal actual fun MppAtomicRef(value: T): MppAtomicRef = JsAtomicRef(value) - - -private interface JsMppImpls { - fun readEnvvar(key: String): String? - fun stdoutInteractive(): Boolean - fun stdinInteractive(): Boolean - fun stderrInteractive(): Boolean - fun getTerminalSize(): Size? - fun printStderr(message: String, newline: Boolean) - fun readLineOrNull(): String? - fun makeTerminalCursor(terminal: Terminal): TerminalCursor -} - -private object BrowserMppImpls : JsMppImpls { - override fun readEnvvar(key: String): String? = null - override fun stdoutInteractive(): Boolean = false - override fun stdinInteractive(): Boolean = false - override fun stderrInteractive(): Boolean = false - override fun getTerminalSize(): Size? = null - override fun printStderr(message: String, newline: Boolean) { - // No way to avoid the newline on browsers - console.error(message) - } - - // readlnOrNull will just throw an exception on browsers - override fun readLineOrNull(): String? = readlnOrNull() - override fun makeTerminalCursor(terminal: Terminal): TerminalCursor { - return BrowserTerminalCursor(terminal) - } +internal actual fun browserPrintln(message: String) { + // No way to avoid the newline on browsers + console.error(message) } -private class NodeMppImpls(private val fs: dynamic) : JsMppImpls { +private class NodeMppImpls(private val fs: dynamic) : BaseNodeMppImpls() { override fun readEnvvar(key: String): String? = process.env[key] as? String override fun stdoutInteractive(): Boolean = js("Boolean(process.stdout.isTTY)") as Boolean override fun stdinInteractive(): Boolean = js("Boolean(process.stdin.isTTY)") as Boolean @@ -83,24 +28,15 @@ private class NodeMppImpls(private val fs: dynamic) : JsMppImpls { } override fun printStderr(message: String, newline: Boolean) { - val s = if (newline) message + "\n" else message - process.stderr.write(s) + process.stderr.write(if (newline) message + "\n" else message) } - override fun readLineOrNull(): String? { - return try { - buildString { - var char: String - val buf = Buffer.alloc(1) - do { - fs.readSync(fd = 0, bufer = buf, offset = 0, len = 1, position = null) - char = "$buf" // don't call toString here due to KT-55817 - append(char) - } while (char != "\n") - } - } catch (e: Exception) { - null - } + override fun allocBuffer(size: Int): dynamic { + return Buffer.alloc(size) + } + + override fun readSync(fd: Int, buffer: dynamic, offset: Int, len: Int): Int { + return fs.readSync(fd, buffer, offset, len, null) as Int } override fun makeTerminalCursor(terminal: Terminal): TerminalCursor { @@ -108,26 +44,14 @@ private class NodeMppImpls(private val fs: dynamic) : JsMppImpls { } } -private val impls: JsMppImpls = try { - NodeMppImpls(nodeRequire("fs")) -} catch (e: Exception) { - BrowserMppImpls -} - -internal actual fun runningInIdeaJavaAgent(): Boolean = false - -internal actual fun getTerminalSize(): Size? = impls.getTerminalSize() -internal actual fun getEnv(key: String): String? = impls.readEnvvar(key) -internal actual fun stdoutInteractive(): Boolean = impls.stdoutInteractive() -internal actual fun stdinInteractive(): Boolean = impls.stdinInteractive() -internal actual fun printStderr(message: String, newline: Boolean) { - impls.printStderr(message, newline) +internal actual fun makeNodeMppImpls(): JsMppImpls? { + return try { + NodeMppImpls(nodeRequire("fs")) + } catch (e: Exception) { + null + } } -// hideInput is not currently implemented -internal actual fun readLineOrNullMpp(hideInput: Boolean): String? = impls.readLineOrNull() - - internal actual fun codepointSequence(string: String): Sequence { val it = string.asDynamic()[Symbol.iterator]() return generateSequence { @@ -135,10 +59,6 @@ internal actual fun codepointSequence(string: String): Sequence { } } -internal actual fun makePrintingTerminalCursor(terminal: Terminal): TerminalCursor { - return impls.makeTerminalCursor(terminal) -} - private class NodeTerminalCursor(terminal: Terminal) : PrintTerminalCursor(terminal) { private var shutdownHook: (() -> Unit)? = null @@ -156,18 +76,4 @@ private class NodeTerminalCursor(terminal: Terminal) : PrintTerminalCursor(termi } } -// There are no shutdown hooks on browsers, so we don't need to do anything here -private class BrowserTerminalCursor(terminal: Terminal) : PrintTerminalCursor(terminal) - - -internal actual fun sendInterceptedPrintRequest( - request: PrintRequest, - terminalInterface: TerminalInterface, - interceptors: List, -) { - terminalInterface.completePrintRequest( - interceptors.fold(request) { acc, it -> it.intercept(acc) } - ) -} - -internal actual val FAST_ISATTY: Boolean = true +internal actual val CR_IMPLIES_LF: Boolean = false diff --git a/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt b/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt index 6afd857d5..ff314d5a2 100644 --- a/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt +++ b/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt @@ -140,3 +140,4 @@ internal actual fun stdoutInteractive(): Boolean = impls.stdoutInteractive() internal actual fun stdinInteractive(): Boolean = impls.stdinInteractive() internal actual fun getTerminalSize(): Size? = impls.getTerminalSize() internal actual val FAST_ISATTY: Boolean = true +internal actual val CR_IMPLIES_LF: Boolean = false diff --git a/mordant/src/jvmTest/kotlin/com/github/ajalt/mordant/animation/DeprecatedProgressAnimationTest.kt b/mordant/src/jvmTest/kotlin/com/github/ajalt/mordant/animation/DeprecatedProgressAnimationTest.kt index 3b83f8627..03e18e0c3 100644 --- a/mordant/src/jvmTest/kotlin/com/github/ajalt/mordant/animation/DeprecatedProgressAnimationTest.kt +++ b/mordant/src/jvmTest/kotlin/com/github/ajalt/mordant/animation/DeprecatedProgressAnimationTest.kt @@ -5,7 +5,7 @@ import com.github.ajalt.mordant.rendering.Theme import com.github.ajalt.mordant.terminal.Terminal import com.github.ajalt.mordant.terminal.TerminalRecorder import com.github.ajalt.mordant.test.RenderingTest -import com.github.ajalt.mordant.test.normalizedOutput +import com.github.ajalt.mordant.test.latestOutput import io.kotest.matchers.shouldBe import kotlin.test.Test import kotlin.time.Duration.Companion.seconds @@ -30,27 +30,27 @@ class DeprecatedProgressAnimationTest : RenderingTest() { } pt.update(0, 1000) - vt.normalizedOutput() shouldBe " ---.-it/s|eta -:--:--" + vt.latestOutput() shouldBe " ---.-it/s|eta -:--:--" now += 0.5.seconds vt.clearOutput() pt.update(40) - vt.normalizedOutput() shouldBe " ---.-it/s|eta -:--:--" + vt.latestOutput() shouldBe " ---.-it/s|eta -:--:--" now += 0.1.seconds vt.clearOutput() pt.update() - vt.normalizedOutput() shouldBe " ---.-it/s|eta -:--:--" + vt.latestOutput() shouldBe " ---.-it/s|eta -:--:--" now += 0.4.seconds vt.clearOutput() pt.update() - vt.normalizedOutput() shouldBe " 40.0it/s|eta 0:00:24" + vt.latestOutput() shouldBe " 40.0it/s|eta 0:00:24" now += 0.9.seconds vt.clearOutput() pt.update() - vt.normalizedOutput() shouldBe " 40.0it/s|eta 0:00:24" + vt.latestOutput() shouldBe " 40.0it/s|eta 0:00:24" } @Test @@ -71,26 +71,26 @@ class DeprecatedProgressAnimationTest : RenderingTest() { timeRemaining() } pt.update(0, 100) - vt.normalizedOutput() shouldBe "text.txt| 0%|......| 0/100| ---.-it/s|eta -:--:--" + vt.latestOutput() shouldBe "text.txt| 0%|......| 0/100| ---.-it/s|eta -:--:--" now += 10.0.seconds vt.clearOutput() pt.update(40) - vt.normalizedOutput() shouldBe "text.txt| 40%|##>...| 40/100| 4.0it/s|eta 0:00:15" + vt.latestOutput() shouldBe "text.txt| 40%|##>...| 40/100| 4.0it/s|eta 0:00:15" now += 10.0.seconds vt.clearOutput() pt.update() - vt.normalizedOutput() shouldBe "text.txt| 40%|##>...| 40/100| 2.0it/s|eta 0:00:30" + vt.latestOutput() shouldBe "text.txt| 40%|##>...| 40/100| 2.0it/s|eta 0:00:30" now += 10.0.seconds vt.clearOutput() pt.updateTotal(200) - vt.normalizedOutput() shouldBe "text.txt| 20%|#>....| 40/200| 1.3it/s|eta 0:02:00" + vt.latestOutput() shouldBe "text.txt| 20%|#>....| 40/200| 1.3it/s|eta 0:02:00" vt.clearOutput() pt.restart() - vt.normalizedOutput() shouldBe "text.txt| 0%|......| 0/200| ---.-it/s|eta -:--:--" + vt.latestOutput() shouldBe "text.txt| 0%|......| 0/200| ---.-it/s|eta -:--:--" vt.clearOutput() pt.clear() diff --git a/mordant/src/nativeMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt b/mordant/src/nativeMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt index fcc1d8f90..cb4b97508 100644 --- a/mordant/src/nativeMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt +++ b/mordant/src/nativeMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt @@ -154,3 +154,4 @@ internal actual fun sendInterceptedPrintRequest( } internal actual val FAST_ISATTY: Boolean = true +internal actual val CR_IMPLIES_LF: Boolean = false diff --git a/mordant/src/wasmJsMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt b/mordant/src/wasmJsMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt new file mode 100644 index 000000000..21e9a5d18 --- /dev/null +++ b/mordant/src/wasmJsMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt @@ -0,0 +1,117 @@ +package com.github.ajalt.mordant.internal + +import com.github.ajalt.mordant.terminal.PrintTerminalCursor +import com.github.ajalt.mordant.terminal.Terminal +import com.github.ajalt.mordant.terminal.TerminalCursor + +private external interface Stream { + val isTTY: Boolean + fun write(s: String) + fun getWindowSize(): JsArray +} + +@Suppress("ClassName") +private external object process { + val stdout: Stream + val stdin: Stream + val stderr: Stream + + fun on(event: String, listener: () -> Unit) + fun removeListener(event: String, listener: () -> Unit) +} + +private external interface FsModule { + fun readSync(fd: Int, buffer: JsAny, offset: Int, len: Int, position: JsAny?): Int +} + +private external object Buffer { + fun alloc(size: Int): JsAny +} + +@Suppress("RedundantNullableReturnType") // invalid diagnostic due to KTIJ-28239 +private fun nodeReadEnvvar(@Suppress("UNUSED_PARAMETER") key: String): String? = + js("process.env[key]") + +private fun nodeWidowSizeIsDefined(): Boolean = + js("process.stdout.getWindowSize != undefined") + +private class NodeMppImpls(private val fs: FsModule) : BaseNodeMppImpls() { + override fun readEnvvar(key: String): String? = nodeReadEnvvar(key) + override fun stdoutInteractive(): Boolean = process.stdout.isTTY + override fun stdinInteractive(): Boolean = process.stdin.isTTY + override fun stderrInteractive(): Boolean = process.stderr.isTTY + override fun getTerminalSize(): Size? { + if (!nodeWidowSizeIsDefined()) return null + val jsSize = process.stdout.getWindowSize() + return Size(width = jsSize[0]!!.toInt(), height = jsSize[1]!!.toInt()) + } + + override fun printStderr(message: String, newline: Boolean) { + process.stderr.write(if (newline) message + "\n" else message) + } + + override fun allocBuffer(size: Int): JsAny = Buffer.alloc(size) + + override fun readSync(fd: Int, buffer: JsAny, offset: Int, len: Int): Int { + return fs.readSync(fd, buffer, offset, len, null) + } + + override fun makeTerminalCursor(terminal: Terminal): TerminalCursor { + return NodeTerminalCursor(terminal) + } +} + +internal actual fun browserPrintln(message: String): Unit = js("console.error(message)") + +internal actual fun makeNodeMppImpls(): JsMppImpls? { + return if (runningOnNode()) NodeMppImpls(importNodeFsModule()) else null +} + +private class NodeTerminalCursor(terminal: Terminal) : PrintTerminalCursor(terminal) { + private var shutdownHook: (() -> Unit)? = null + + override fun show() { + shutdownHook?.let { process.removeListener("exit", it) } + super.show() + } + + override fun hide(showOnExit: Boolean) { + if (showOnExit && shutdownHook == null) { + val function = { show() } + shutdownHook = function + process.on("exit", function) + } + super.hide(showOnExit) + } +} + + +private external interface CodePointString { + fun codePointAt(index: Int): Int +} + +private external interface StringIteration { + val value: CodePointString? +} + +private external interface StringIterator { + fun next(): StringIteration +} + +private fun stringIterator(@Suppress("UNUSED_PARAMETER") s: String): StringIterator = + js("s[Symbol.iterator]()") + +internal actual fun codepointSequence(string: String): Sequence { + val it = stringIterator(string) + return generateSequence { it.next().value?.codePointAt(0) } +} + +// See jsMain/MppImpl.kt for the details of node detection +private fun runningOnNode(): Boolean = + js("Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'") + +private fun importNodeFsModule(): FsModule = + js("""require("fs")""") + +// For some reason, \r seems to be treated as \r\n on wasm +internal actual val CR_IMPLIES_LF: Boolean = true diff --git a/samples/markdown/build.gradle.kts b/samples/markdown/build.gradle.kts index b4788949b..8e682dfef 100644 --- a/samples/markdown/build.gradle.kts +++ b/samples/markdown/build.gradle.kts @@ -1,3 +1,3 @@ plugins { - id("mordant-mpp-sample-conventions") + id("mordant-jvm-sample-conventions") } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7c7b9ab31..57d817a93 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,11 @@ include( dependencyResolutionManagement { repositories { mavenCentral() + // TODO: we can remove this once kotest releases a new version + maven { + url= uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + mavenContent { snapshotsOnly() } + } } }