Repository: ajalt/mordant
Pull request #219 (2024-08-31 17:23:18)

Fix using numbers with different bit widths


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 199c920f..98fe1c60 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,9 +20,9 @@ jobs:
         os: [macos-latest, windows-latest, ubuntu-latest]
         include:
           - os: ubuntu-latest
-            EXTRA_GRADLE_ARGS: :test:proguard:r8jar apiCheck
+            EXTRA_GRADLE_ARGS: apiCheck :test:proguard:r8jar
           - os: macos-latest
-            EXTRA_GRADLE_ARGS: :mordant:compilePosixMainKotlinMetadata
+            EXTRA_GRADLE_ARGS: :mordant:compileNativeMainKotlinMetadata :mordant:compilePosixMainKotlinMetadata :mordant:compileAppleMainKotlinMetadata
     runs-on: ${{matrix.os}}
     steps:
       - uses: actions/checkout@v4
@@ -39,11 +39,11 @@ jobs:
       - name: Run tests
         run: >-
             ./gradlew
+            ${{matrix.EXTRA_GRADLE_ARGS}}
             :mordant:check
             :mordant-coroutines:check
             :mordant-markdown:check
             :test:graalvm:nativeTest
-            ${{matrix.EXTRA_GRADLE_ARGS}}
             --stacktrace
       - name: Run R8 Jar
         if: ${{ matrix.os == 'ubuntu-latest' }}
diff --git a/buildSrc/src/main/kotlin/mordant-native-conventions.gradle.kts b/buildSrc/src/main/kotlin/mordant-native-conventions.gradle.kts
index 44bf8aee..da143de6 100644
--- a/buildSrc/src/main/kotlin/mordant-native-conventions.gradle.kts
+++ b/buildSrc/src/main/kotlin/mordant-native-conventions.gradle.kts
@@ -8,7 +8,6 @@ kotlin {
     tvosX64()
     tvosArm64()
     tvosSimulatorArm64()
-    watchosArm32()
     watchosArm64()
     watchosX64()
 }
diff --git a/mordant/src/appleNonDesktopMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.appleNonDesktop.kt b/mordant/src/appleNonDesktopMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.appleNonDesktop.kt
index c741c066..8ab0c23b 100644
--- a/mordant/src/appleNonDesktopMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.appleNonDesktop.kt
+++ b/mordant/src/appleNonDesktopMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.appleNonDesktop.kt
@@ -1,3 +1,3 @@
 package com.github.ajalt.mordant.internal
 
-internal actual fun hasFileSystem(): Boolean = false
+internal actual fun testsHaveFileSystem(): Boolean = false
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.kt
index 48d11081..5087f2a5 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.kt
@@ -53,7 +53,8 @@ internal expect fun exitProcessMpp(status: Int)
 
 internal expect fun readFileIfExists(filename: String): String?
 
-internal expect fun hasFileSystem(): Boolean
+/** Whether tests running on this platform can access the filesystem */
+internal expect fun testsHaveFileSystem(): Boolean
 
 internal expect fun getStandardTerminalInterface(): TerminalInterface
 
diff --git a/mordant/src/appleMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.apple.kt b/mordant/src/iosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.ios.kt
similarity index 76%
rename from mordant/src/appleMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.apple.kt
rename to mordant/src/iosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.ios.kt
index 672faed6..256b039a 100644
--- a/mordant/src/appleMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.apple.kt
+++ b/mordant/src/iosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.ios.kt
@@ -1,8 +1,8 @@
 package com.github.ajalt.mordant.internal
 
 import com.github.ajalt.mordant.terminal.TerminalInterface
-import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeApple
+import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeCopyPasted
 
 internal actual fun getStandardTerminalInterface(): TerminalInterface {
-    return TerminalInterfaceNativeApple()
+    return TerminalInterfaceNativeCopyPasted()
 }
diff --git a/mordant/src/appleMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.apple.kt b/mordant/src/iosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.ios.kt
similarity index 60%
rename from mordant/src/appleMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.apple.kt
rename to mordant/src/iosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.ios.kt
index eb2fb36a..a1ed5922 100644
--- a/mordant/src/appleMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.apple.kt
+++ b/mordant/src/iosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.ios.kt
@@ -4,11 +4,11 @@ import com.github.ajalt.mordant.rendering.Size
 import kotlinx.cinterop.*
 import platform.posix.*
 
-// The source code for this file is identical between linux and apple targets, but they have
-// different bit widths for some of the termios fields, so the compileMetadata task would fail if we
-// don't use separate files.
+// XXX: The source code for this file is identical between linux and the various apple targets, but
+// they have different bit widths for some fields, so the compileMetadata task fails if we don't use
+// separate files. Hopefully some day there will be solution that doesn't require copy-pasting.
 
-internal class TerminalInterfaceNativeApple : TerminalInterfaceNativePosix() {
+internal class TerminalInterfaceNativeCopyPasted : TerminalInterfaceNativePosix() {
     override val termiosConstants: TermiosConstants = TermiosConstants(
         VTIME = VTIME,
         VMIN = VMIN,
@@ -32,7 +32,7 @@ internal class TerminalInterfaceNativeApple : TerminalInterfaceNativePosix() {
 
     override fun getTerminalSize(): Size? = memScoped {
         val size = alloc<winsize>()
-        if (ioctl(STDIN_FILENO, TIOCGWINSZ.toULong(), size) < 0) {
+        if (ioctl(STDIN_FILENO, TIOCGWINSZ.convert(), size) < 0) {
             null
         } else {
             Size(width = size.ws_col.toInt(), height = size.ws_row.toInt())
@@ -41,15 +41,15 @@ internal class TerminalInterfaceNativeApple : TerminalInterfaceNativePosix() {
 
     override fun getStdinTermios(): Termios = memScoped {
         val termios = alloc<termios>()
-        if (tcgetattr(STDIN_FILENO, termios.ptr) != 0) {
+        if (tcgetattr(platform.posix.STDIN_FILENO, termios.ptr) != 0) {
             throw RuntimeException("Error reading terminal attributes")
         }
         return Termios(
-            iflag = termios.c_iflag.convert<UInt>(),
-            oflag = termios.c_oflag.convert<UInt>(),
-            cflag = termios.c_cflag.convert<UInt>(),
-            lflag = termios.c_lflag.convert<UInt>(),
-            cc = ByteArray(NCCS) { termios.c_cc[it].convert<Byte>() },
+            iflag = termios.c_iflag.convert(),
+            oflag = termios.c_oflag.convert(),
+            cflag = termios.c_cflag.convert(),
+            lflag = termios.c_lflag.convert(),
+            cc = ByteArray(NCCS) { termios.c_cc[it].convert() },
         )
     }
 
@@ -57,15 +57,15 @@ internal class TerminalInterfaceNativeApple : TerminalInterfaceNativePosix() {
         val nativeTermios = alloc<termios>()
         // different platforms have different fields in termios, so we need to read the current
         // struct before we set the fields we care about.
-        if (tcgetattr(STDIN_FILENO, nativeTermios.ptr) != 0) {
+        if (tcgetattr(platform.posix.STDIN_FILENO, nativeTermios.ptr) != 0) {
             throw RuntimeException("Error reading terminal attributes")
         }
-        nativeTermios.c_iflag = termios.iflag.convert<tcflag_t>()
-        nativeTermios.c_oflag = termios.oflag.convert<tcflag_t>()
-        nativeTermios.c_cflag = termios.cflag.convert<tcflag_t>()
-        nativeTermios.c_lflag = termios.lflag.convert<tcflag_t>()
+        nativeTermios.c_iflag = termios.iflag.convert()
+        nativeTermios.c_oflag = termios.oflag.convert()
+        nativeTermios.c_cflag = termios.cflag.convert()
+        nativeTermios.c_lflag = termios.lflag.convert()
         repeat(NCCS) { nativeTermios.c_cc[it] = termios.cc[it].convert<UByte>() }
-        if (tcsetattr(STDIN_FILENO, TCSADRAIN, nativeTermios.ptr) != 0) {
+        if (tcsetattr(platform.posix.STDIN_FILENO, TCSADRAIN, nativeTermios.ptr) != 0) {
             throw RuntimeException("Error setting terminal attributes")
         }
     }
diff --git a/mordant/src/jsCommonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jsCommon.kt b/mordant/src/jsCommonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jsCommon.kt
index 46306831..b9ba914d 100644
--- a/mordant/src/jsCommonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jsCommon.kt
+++ b/mordant/src/jsCommonMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jsCommon.kt
@@ -79,4 +79,4 @@ internal actual fun sendInterceptedPrintRequest(
     )
 }
 
-internal actual fun hasFileSystem(): Boolean = impls !is TerminalInterfaceBrowser
+internal actual fun testsHaveFileSystem(): Boolean = impls !is TerminalInterfaceBrowser
diff --git a/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jvm.kt b/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jvm.kt
index 8996abc6..31487341 100644
--- a/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jvm.kt
+++ b/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jvm.kt
@@ -143,7 +143,7 @@ internal actual fun getStandardTerminalInterface(): TerminalInterface {
 }
 
 internal actual val CR_IMPLIES_LF: Boolean = false
-internal actual fun hasFileSystem(): Boolean = true
+internal actual fun testsHaveFileSystem(): Boolean = true
 
 internal actual fun exitProcessMpp(status: Int) {
     exitProcess(status)
diff --git a/mordant/src/linuxMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.linux.kt b/mordant/src/linuxMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.linux.kt
index a4865421..7d768baf 100644
--- a/mordant/src/linuxMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.linux.kt
+++ b/mordant/src/linuxMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.linux.kt
@@ -1,7 +1,9 @@
 package com.github.ajalt.mordant.internal
 
 import com.github.ajalt.mordant.terminal.TerminalInterface
-import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeLinux
+import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeCopyPasted
 
-internal actual fun hasFileSystem(): Boolean = true
-internal actual fun getStandardTerminalInterface(): TerminalInterface = TerminalInterfaceNativeLinux
+internal actual fun testsHaveFileSystem(): Boolean = true
+internal actual fun getStandardTerminalInterface(): TerminalInterface {
+    return TerminalInterfaceNativeCopyPasted()
+}
diff --git a/mordant/src/linuxMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.linux.kt b/mordant/src/linuxMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.linux.kt
index 3a39cdcb..a1ed5922 100644
--- a/mordant/src/linuxMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.linux.kt
+++ b/mordant/src/linuxMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.linux.kt
@@ -4,11 +4,11 @@ import com.github.ajalt.mordant.rendering.Size
 import kotlinx.cinterop.*
 import platform.posix.*
 
-// The source code for this file is identical between linux and apple targets, but they have
-// different bit widths for some of the termios fields, so the compileMetadata task would fail if we
-// don't use separate files.
+// XXX: The source code for this file is identical between linux and the various apple targets, but
+// they have different bit widths for some fields, so the compileMetadata task fails if we don't use
+// separate files. Hopefully some day there will be solution that doesn't require copy-pasting.
 
-internal object TerminalInterfaceNativeLinux : TerminalInterfaceNativePosix() {
+internal class TerminalInterfaceNativeCopyPasted : TerminalInterfaceNativePosix() {
     override val termiosConstants: TermiosConstants = TermiosConstants(
         VTIME = VTIME,
         VMIN = VMIN,
@@ -32,7 +32,7 @@ internal object TerminalInterfaceNativeLinux : TerminalInterfaceNativePosix() {
 
     override fun getTerminalSize(): Size? = memScoped {
         val size = alloc<winsize>()
-        if (ioctl(STDIN_FILENO, TIOCGWINSZ.toULong(), size) < 0) {
+        if (ioctl(STDIN_FILENO, TIOCGWINSZ.convert(), size) < 0) {
             null
         } else {
             Size(width = size.ws_col.toInt(), height = size.ws_row.toInt())
@@ -45,11 +45,11 @@ internal object TerminalInterfaceNativeLinux : TerminalInterfaceNativePosix() {
             throw RuntimeException("Error reading terminal attributes")
         }
         return Termios(
-            iflag = termios.c_iflag.convert<UInt>(),
-            oflag = termios.c_oflag.convert<UInt>(),
-            cflag = termios.c_cflag.convert<UInt>(),
-            lflag = termios.c_lflag.convert<UInt>(),
-            cc = ByteArray(NCCS) { termios.c_cc[it].convert<Byte>() },
+            iflag = termios.c_iflag.convert(),
+            oflag = termios.c_oflag.convert(),
+            cflag = termios.c_cflag.convert(),
+            lflag = termios.c_lflag.convert(),
+            cc = ByteArray(NCCS) { termios.c_cc[it].convert() },
         )
     }
 
@@ -60,10 +60,10 @@ internal object TerminalInterfaceNativeLinux : TerminalInterfaceNativePosix() {
         if (tcgetattr(platform.posix.STDIN_FILENO, nativeTermios.ptr) != 0) {
             throw RuntimeException("Error reading terminal attributes")
         }
-        nativeTermios.c_iflag = termios.iflag.convert<tcflag_t>()
-        nativeTermios.c_oflag = termios.oflag.convert<tcflag_t>()
-        nativeTermios.c_cflag = termios.cflag.convert<tcflag_t>()
-        nativeTermios.c_lflag = termios.lflag.convert<tcflag_t>()
+        nativeTermios.c_iflag = termios.iflag.convert()
+        nativeTermios.c_oflag = termios.oflag.convert()
+        nativeTermios.c_cflag = termios.cflag.convert()
+        nativeTermios.c_lflag = termios.lflag.convert()
         repeat(NCCS) { nativeTermios.c_cc[it] = termios.cc[it].convert<UByte>() }
         if (tcsetattr(platform.posix.STDIN_FILENO, TCSADRAIN, nativeTermios.ptr) != 0) {
             throw RuntimeException("Error setting terminal attributes")
diff --git a/mordant/src/macosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.macos.kt b/mordant/src/macosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.macos.kt
index 50ea5706..7d768baf 100644
--- a/mordant/src/macosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.macos.kt
+++ b/mordant/src/macosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.macos.kt
@@ -1,3 +1,9 @@
 package com.github.ajalt.mordant.internal
 
-internal actual fun hasFileSystem(): Boolean = true
+import com.github.ajalt.mordant.terminal.TerminalInterface
+import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeCopyPasted
+
+internal actual fun testsHaveFileSystem(): Boolean = true
+internal actual fun getStandardTerminalInterface(): TerminalInterface {
+    return TerminalInterfaceNativeCopyPasted()
+}
diff --git a/mordant/src/macosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.linux.kt b/mordant/src/macosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.linux.kt
new file mode 100644
index 00000000..a1ed5922
--- /dev/null
+++ b/mordant/src/macosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.linux.kt
@@ -0,0 +1,72 @@
+package com.github.ajalt.mordant.terminal.terminalinterface
+
+import com.github.ajalt.mordant.rendering.Size
+import kotlinx.cinterop.*
+import platform.posix.*
+
+// XXX: The source code for this file is identical between linux and the various apple targets, but
+// they have different bit widths for some fields, so the compileMetadata task fails if we don't use
+// separate files. Hopefully some day there will be solution that doesn't require copy-pasting.
+
+internal class TerminalInterfaceNativeCopyPasted : TerminalInterfaceNativePosix() {
+    override val termiosConstants: TermiosConstants = TermiosConstants(
+        VTIME = VTIME,
+        VMIN = VMIN,
+        INPCK = INPCK.convert(),
+        ISTRIP = ISTRIP.convert(),
+        INLCR = INLCR.convert(),
+        IGNCR = IGNCR.convert(),
+        ICRNL = ICRNL.convert(),
+        IXON = IXON.convert(),
+        OPOST = OPOST.convert(),
+        CS8 = CS8.convert(),
+        ISIG = ISIG.convert(),
+        ICANON = ICANON.convert(),
+        ECHO = ECHO.convert(),
+        IEXTEN = IEXTEN.convert(),
+    )
+
+    override fun readIntoBuffer(c: ByteVar): Long {
+        return read(platform.posix.STDIN_FILENO, c.ptr, 1u).convert()
+    }
+
+    override fun getTerminalSize(): Size? = memScoped {
+        val size = alloc<winsize>()
+        if (ioctl(STDIN_FILENO, TIOCGWINSZ.convert(), size) < 0) {
+            null
+        } else {
+            Size(width = size.ws_col.toInt(), height = size.ws_row.toInt())
+        }
+    }
+
+    override fun getStdinTermios(): Termios = memScoped {
+        val termios = alloc<termios>()
+        if (tcgetattr(platform.posix.STDIN_FILENO, termios.ptr) != 0) {
+            throw RuntimeException("Error reading terminal attributes")
+        }
+        return Termios(
+            iflag = termios.c_iflag.convert(),
+            oflag = termios.c_oflag.convert(),
+            cflag = termios.c_cflag.convert(),
+            lflag = termios.c_lflag.convert(),
+            cc = ByteArray(NCCS) { termios.c_cc[it].convert() },
+        )
+    }
+
+    override fun setStdinTermios(termios: Termios) = memScoped {
+        val nativeTermios = alloc<termios>()
+        // different platforms have different fields in termios, so we need to read the current
+        // struct before we set the fields we care about.
+        if (tcgetattr(platform.posix.STDIN_FILENO, nativeTermios.ptr) != 0) {
+            throw RuntimeException("Error reading terminal attributes")
+        }
+        nativeTermios.c_iflag = termios.iflag.convert()
+        nativeTermios.c_oflag = termios.oflag.convert()
+        nativeTermios.c_cflag = termios.cflag.convert()
+        nativeTermios.c_lflag = termios.lflag.convert()
+        repeat(NCCS) { nativeTermios.c_cc[it] = termios.cc[it].convert<UByte>() }
+        if (tcsetattr(platform.posix.STDIN_FILENO, TCSADRAIN, nativeTermios.ptr) != 0) {
+            throw RuntimeException("Error setting terminal attributes")
+        }
+    }
+}
diff --git a/mordant/src/mingwMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.mingw.kt b/mordant/src/mingwMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.mingw.kt
index 216ce2ec..7902250f 100644
--- a/mordant/src/mingwMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.mingw.kt
+++ b/mordant/src/mingwMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.mingw.kt
@@ -4,6 +4,6 @@ import com.github.ajalt.mordant.terminal.TerminalInterface
 import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeWindows
 
 internal actual fun ttySetEcho(echo: Boolean) = TerminalInterfaceNativeWindows.ttySetEcho(echo)
-internal actual fun hasFileSystem(): Boolean = true
+internal actual fun testsHaveFileSystem(): Boolean = true
 internal actual fun getStandardTerminalInterface(): TerminalInterface =
     TerminalInterfaceNativeWindows
diff --git a/mordant/src/tvosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.tvos.kt b/mordant/src/tvosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.tvos.kt
new file mode 100644
index 00000000..256b039a
--- /dev/null
+++ b/mordant/src/tvosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.tvos.kt
@@ -0,0 +1,8 @@
+package com.github.ajalt.mordant.internal
+
+import com.github.ajalt.mordant.terminal.TerminalInterface
+import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeCopyPasted
+
+internal actual fun getStandardTerminalInterface(): TerminalInterface {
+    return TerminalInterfaceNativeCopyPasted()
+}
diff --git a/mordant/src/tvosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.tvos.kt b/mordant/src/tvosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.tvos.kt
new file mode 100644
index 00000000..a1ed5922
--- /dev/null
+++ b/mordant/src/tvosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.tvos.kt
@@ -0,0 +1,72 @@
+package com.github.ajalt.mordant.terminal.terminalinterface
+
+import com.github.ajalt.mordant.rendering.Size
+import kotlinx.cinterop.*
+import platform.posix.*
+
+// XXX: The source code for this file is identical between linux and the various apple targets, but
+// they have different bit widths for some fields, so the compileMetadata task fails if we don't use
+// separate files. Hopefully some day there will be solution that doesn't require copy-pasting.
+
+internal class TerminalInterfaceNativeCopyPasted : TerminalInterfaceNativePosix() {
+    override val termiosConstants: TermiosConstants = TermiosConstants(
+        VTIME = VTIME,
+        VMIN = VMIN,
+        INPCK = INPCK.convert(),
+        ISTRIP = ISTRIP.convert(),
+        INLCR = INLCR.convert(),
+        IGNCR = IGNCR.convert(),
+        ICRNL = ICRNL.convert(),
+        IXON = IXON.convert(),
+        OPOST = OPOST.convert(),
+        CS8 = CS8.convert(),
+        ISIG = ISIG.convert(),
+        ICANON = ICANON.convert(),
+        ECHO = ECHO.convert(),
+        IEXTEN = IEXTEN.convert(),
+    )
+
+    override fun readIntoBuffer(c: ByteVar): Long {
+        return read(platform.posix.STDIN_FILENO, c.ptr, 1u).convert()
+    }
+
+    override fun getTerminalSize(): Size? = memScoped {
+        val size = alloc<winsize>()
+        if (ioctl(STDIN_FILENO, TIOCGWINSZ.convert(), size) < 0) {
+            null
+        } else {
+            Size(width = size.ws_col.toInt(), height = size.ws_row.toInt())
+        }
+    }
+
+    override fun getStdinTermios(): Termios = memScoped {
+        val termios = alloc<termios>()
+        if (tcgetattr(platform.posix.STDIN_FILENO, termios.ptr) != 0) {
+            throw RuntimeException("Error reading terminal attributes")
+        }
+        return Termios(
+            iflag = termios.c_iflag.convert(),
+            oflag = termios.c_oflag.convert(),
+            cflag = termios.c_cflag.convert(),
+            lflag = termios.c_lflag.convert(),
+            cc = ByteArray(NCCS) { termios.c_cc[it].convert() },
+        )
+    }
+
+    override fun setStdinTermios(termios: Termios) = memScoped {
+        val nativeTermios = alloc<termios>()
+        // different platforms have different fields in termios, so we need to read the current
+        // struct before we set the fields we care about.
+        if (tcgetattr(platform.posix.STDIN_FILENO, nativeTermios.ptr) != 0) {
+            throw RuntimeException("Error reading terminal attributes")
+        }
+        nativeTermios.c_iflag = termios.iflag.convert()
+        nativeTermios.c_oflag = termios.oflag.convert()
+        nativeTermios.c_cflag = termios.cflag.convert()
+        nativeTermios.c_lflag = termios.lflag.convert()
+        repeat(NCCS) { nativeTermios.c_cc[it] = termios.cc[it].convert<UByte>() }
+        if (tcsetattr(platform.posix.STDIN_FILENO, TCSADRAIN, nativeTermios.ptr) != 0) {
+            throw RuntimeException("Error setting terminal attributes")
+        }
+    }
+}
diff --git a/mordant/src/watchosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.watchos.kt b/mordant/src/watchosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.watchos.kt
new file mode 100644
index 00000000..256b039a
--- /dev/null
+++ b/mordant/src/watchosMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.watchos.kt
@@ -0,0 +1,8 @@
+package com.github.ajalt.mordant.internal
+
+import com.github.ajalt.mordant.terminal.TerminalInterface
+import com.github.ajalt.mordant.terminal.terminalinterface.TerminalInterfaceNativeCopyPasted
+
+internal actual fun getStandardTerminalInterface(): TerminalInterface {
+    return TerminalInterfaceNativeCopyPasted()
+}
diff --git a/mordant/src/watchosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.watchos.kt b/mordant/src/watchosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.watchos.kt
new file mode 100644
index 00000000..a1ed5922
--- /dev/null
+++ b/mordant/src/watchosMain/kotlin/com/github/ajalt/mordant/terminal/terminalinterface/TerminalInterface.native.watchos.kt
@@ -0,0 +1,72 @@
+package com.github.ajalt.mordant.terminal.terminalinterface
+
+import com.github.ajalt.mordant.rendering.Size
+import kotlinx.cinterop.*
+import platform.posix.*
+
+// XXX: The source code for this file is identical between linux and the various apple targets, but
+// they have different bit widths for some fields, so the compileMetadata task fails if we don't use
+// separate files. Hopefully some day there will be solution that doesn't require copy-pasting.
+
+internal class TerminalInterfaceNativeCopyPasted : TerminalInterfaceNativePosix() {
+    override val termiosConstants: TermiosConstants = TermiosConstants(
+        VTIME = VTIME,
+        VMIN = VMIN,
+        INPCK = INPCK.convert(),
+        ISTRIP = ISTRIP.convert(),
+        INLCR = INLCR.convert(),
+        IGNCR = IGNCR.convert(),
+        ICRNL = ICRNL.convert(),
+        IXON = IXON.convert(),
+        OPOST = OPOST.convert(),
+        CS8 = CS8.convert(),
+        ISIG = ISIG.convert(),
+        ICANON = ICANON.convert(),
+        ECHO = ECHO.convert(),
+        IEXTEN = IEXTEN.convert(),
+    )
+
+    override fun readIntoBuffer(c: ByteVar): Long {
+        return read(platform.posix.STDIN_FILENO, c.ptr, 1u).convert()
+    }
+
+    override fun getTerminalSize(): Size? = memScoped {
+        val size = alloc<winsize>()
+        if (ioctl(STDIN_FILENO, TIOCGWINSZ.convert(), size) < 0) {
+            null
+        } else {
+            Size(width = size.ws_col.toInt(), height = size.ws_row.toInt())
+        }
+    }
+
+    override fun getStdinTermios(): Termios = memScoped {
+        val termios = alloc<termios>()
+        if (tcgetattr(platform.posix.STDIN_FILENO, termios.ptr) != 0) {
+            throw RuntimeException("Error reading terminal attributes")
+        }
+        return Termios(
+            iflag = termios.c_iflag.convert(),
+            oflag = termios.c_oflag.convert(),
+            cflag = termios.c_cflag.convert(),
+            lflag = termios.c_lflag.convert(),
+            cc = ByteArray(NCCS) { termios.c_cc[it].convert() },
+        )
+    }
+
+    override fun setStdinTermios(termios: Termios) = memScoped {
+        val nativeTermios = alloc<termios>()
+        // different platforms have different fields in termios, so we need to read the current
+        // struct before we set the fields we care about.
+        if (tcgetattr(platform.posix.STDIN_FILENO, nativeTermios.ptr) != 0) {
+            throw RuntimeException("Error reading terminal attributes")
+        }
+        nativeTermios.c_iflag = termios.iflag.convert()
+        nativeTermios.c_oflag = termios.oflag.convert()
+        nativeTermios.c_cflag = termios.cflag.convert()
+        nativeTermios.c_lflag = termios.lflag.convert()
+        repeat(NCCS) { nativeTermios.c_cc[it] = termios.cc[it].convert<UByte>() }
+        if (tcsetattr(platform.posix.STDIN_FILENO, TCSADRAIN, nativeTermios.ptr) != 0) {
+            throw RuntimeException("Error setting terminal attributes")
+        }
+    }
+}


Test patch:
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/platform/MultiplatformSystemTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/platform/MultiplatformSystemTest.kt
index 5b3ca3f5..19e8032a 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/platform/MultiplatformSystemTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/platform/MultiplatformSystemTest.kt
@@ -1,6 +1,6 @@
 package com.github.ajalt.mordant.platform
 
-import com.github.ajalt.mordant.internal.hasFileSystem
+import com.github.ajalt.mordant.internal.testsHaveFileSystem
 import io.kotest.matchers.nulls.shouldNotBeNull
 import io.kotest.matchers.shouldBe
 import io.kotest.matchers.string.shouldNotBeEmpty
@@ -13,7 +13,7 @@ class MultiplatformSystemTest {
         // The kotlin.test plugin doesn't provide a way to set environment variables that works on
         // all targets, so just pick a common one that should exist everywhere.
         val actual = MultiplatformSystem.readEnvironmentVariable("PATH")
-        if (!hasFileSystem()) return
+        if (!testsHaveFileSystem()) return
         actual.shouldNotBeNull().shouldNotBeEmpty()
     }
 
@@ -26,7 +26,7 @@ class MultiplatformSystemTest {
             // js targets have a cwd of $projectDir/build/js/packages/mordant-mordant-test
             "../../../../mordant/src/commonTest/resources/multiplatform_system_test.txt"
         )
-        if (!hasFileSystem()) return
+        if (!testsHaveFileSystem()) return
         actual?.trim() shouldBe "pass"
     }
 }