fixed terminal problems in lyng CLI with new console support on linux
This commit is contained in:
parent
fd6d05d568
commit
2414da59a7
@ -22,11 +22,14 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import platform.posix.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
internal actual fun getNativeSystemConsole(): LyngConsole = LinuxPosixLyngConsole
|
||||
|
||||
private const val RAW_IDLE_POLL_MS = 10L
|
||||
private const val NON_RAW_IDLE_POLL_MS = 25L
|
||||
private const val ESCAPE_FOLLOWUP_TIMEOUT_MS = 25L
|
||||
|
||||
internal object LinuxConsoleKeyDecoder {
|
||||
fun decode(firstCode: Int, nextCode: (Long) -> Int?): ConsoleEvent.KeyDown {
|
||||
if (firstCode == 27) {
|
||||
@ -132,6 +135,7 @@ object LinuxPosixLyngConsole : LyngConsole {
|
||||
}
|
||||
}
|
||||
|
||||
consoleFlowDebug("linux-events: source created")
|
||||
return object : ConsoleEventSource {
|
||||
var closed = false
|
||||
var lastGeometry: ConsoleGeometry? = null
|
||||
@ -147,23 +151,27 @@ object LinuxPosixLyngConsole : LyngConsole {
|
||||
}
|
||||
|
||||
val rawRequested = stateMutex.withLock { rawModeRequested }
|
||||
val pollSliceMs = if (timeoutMs <= 0L) 250L else minOf(250L, timeoutMs)
|
||||
if (rawRequested) {
|
||||
val ev = readKeyEvent(pollSliceMs)
|
||||
val ev = readKeyEventNonBlocking()
|
||||
if (ev != null) return ev
|
||||
} else {
|
||||
delay(25)
|
||||
}
|
||||
|
||||
if (timeoutMs > 0L && started.elapsedNow() >= timeoutMs.milliseconds) {
|
||||
val elapsedMs = started.elapsedNow().inWholeMilliseconds
|
||||
if (timeoutMs > 0L && elapsedMs >= timeoutMs) {
|
||||
return null
|
||||
}
|
||||
|
||||
val remainingMs = if (timeoutMs > 0L) timeoutMs - elapsedMs else Long.MAX_VALUE
|
||||
val idleMs = if (rawRequested) RAW_IDLE_POLL_MS else NON_RAW_IDLE_POLL_MS
|
||||
val sleepMs = if (timeoutMs > 0L) minOf(idleMs, remainingMs) else idleMs
|
||||
if (sleepMs > 0L) delay(sleepMs)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
closed = true
|
||||
consoleFlowDebug("linux-events: source closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,12 +191,11 @@ object LinuxPosixLyngConsole : LyngConsole {
|
||||
}
|
||||
savedAttrsBlob = saved
|
||||
|
||||
attrs.c_lflag = attrs.c_lflag and ICANON.convert<UInt>().inv() and ECHO.convert<UInt>().inv()
|
||||
attrs.c_iflag = attrs.c_iflag and IXON.convert<UInt>().inv() and ISTRIP.convert<UInt>().inv()
|
||||
attrs.c_oflag = attrs.c_oflag and OPOST.convert<UInt>().inv()
|
||||
configureRawInput(attrs)
|
||||
if (tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr) != 0) return@withLock false
|
||||
}
|
||||
rawModeRequested = true
|
||||
consoleFlowDebug("linux-events: setRawMode(true): enabled")
|
||||
true
|
||||
} else {
|
||||
val hadRaw = rawModeRequested
|
||||
@ -203,6 +210,7 @@ object LinuxPosixLyngConsole : LyngConsole {
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr)
|
||||
}
|
||||
}
|
||||
consoleFlowDebug("linux-events: setRawMode(false): disabled hadRaw=$hadRaw")
|
||||
hadRaw
|
||||
}
|
||||
}
|
||||
@ -225,19 +233,55 @@ object LinuxPosixLyngConsole : LyngConsole {
|
||||
val ready = poll(pfd.ptr, 1.convert(), timeoutMs.toInt())
|
||||
if (ready <= 0) return null
|
||||
|
||||
readReadyByte()
|
||||
}
|
||||
|
||||
private fun readReadyByte(): Int? {
|
||||
val buf = ByteArray(1)
|
||||
val count = buf.usePinned { pinned ->
|
||||
read(STDIN_FILENO, pinned.addressOf(0), 1.convert())
|
||||
}
|
||||
if (count <= 0) return null
|
||||
if (count <= 0) {
|
||||
if (count < 0) {
|
||||
consoleFlowDebug("linux-events: stdin read returned $count errno=$errno")
|
||||
}
|
||||
return null
|
||||
}
|
||||
val b = buf[0].toInt()
|
||||
if (b < 0) b + 256 else b
|
||||
return if (b < 0) b + 256 else b
|
||||
}
|
||||
|
||||
private fun readKeyEvent(timeoutMs: Long): ConsoleEvent.KeyDown? {
|
||||
val first = readByte(timeoutMs) ?: return null
|
||||
private fun readByteNow(): Int? = memScoped {
|
||||
val pfd = alloc<pollfd>()
|
||||
pfd.fd = STDIN_FILENO
|
||||
pfd.events = POLLIN.convert()
|
||||
pfd.revents = 0
|
||||
val ready = poll(pfd.ptr, 1.convert(), 0)
|
||||
if (ready <= 0) return null
|
||||
readReadyByte()
|
||||
}
|
||||
|
||||
private fun readKeyEventNonBlocking(): ConsoleEvent.KeyDown? {
|
||||
val first = readByteNow() ?: return null
|
||||
return LinuxConsoleKeyDecoder.decode(first) { timeout ->
|
||||
readByte(timeout)
|
||||
readByte(minOf(timeout, ESCAPE_FOLLOWUP_TIMEOUT_MS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
internal fun configureRawInput(attrs: termios) {
|
||||
attrs.c_iflag = attrs.c_iflag and BRKINT.convert<UInt>().inv()
|
||||
attrs.c_iflag = attrs.c_iflag and ICRNL.convert<UInt>().inv()
|
||||
attrs.c_iflag = attrs.c_iflag and INPCK.convert<UInt>().inv()
|
||||
attrs.c_iflag = attrs.c_iflag and ISTRIP.convert<UInt>().inv()
|
||||
attrs.c_iflag = attrs.c_iflag and IXON.convert<UInt>().inv()
|
||||
attrs.c_oflag = attrs.c_oflag and OPOST.convert<UInt>().inv()
|
||||
attrs.c_cflag = attrs.c_cflag or CS8.convert<UInt>()
|
||||
attrs.c_lflag = attrs.c_lflag and ECHO.convert<UInt>().inv()
|
||||
attrs.c_lflag = attrs.c_lflag and ICANON.convert<UInt>().inv()
|
||||
attrs.c_lflag = attrs.c_lflag and IEXTEN.convert<UInt>().inv()
|
||||
attrs.c_lflag = attrs.c_lflag and ISIG.convert<UInt>().inv()
|
||||
attrs.c_cc[VMIN] = 0u
|
||||
attrs.c_cc[VTIME] = 1u
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
|
||||
package net.sergeych.lyngio.console
|
||||
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
@ -64,4 +66,41 @@ class LinuxPosixLyngConsoleTest {
|
||||
assertEquals("A", ev.key)
|
||||
assertTrue(ev.shift)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
@Test
|
||||
fun configuresRawModeReadSemantics() = memScoped {
|
||||
val attrs = alloc<termios>()
|
||||
attrs.c_iflag =
|
||||
BRKINT.convert<UInt>() or
|
||||
ICRNL.convert<UInt>() or
|
||||
INPCK.convert<UInt>() or
|
||||
ISTRIP.convert<UInt>() or
|
||||
IXON.convert<UInt>()
|
||||
attrs.c_oflag = OPOST.convert<UInt>()
|
||||
attrs.c_cflag = 0u
|
||||
attrs.c_lflag =
|
||||
ECHO.convert<UInt>() or
|
||||
ICANON.convert<UInt>() or
|
||||
IEXTEN.convert<UInt>() or
|
||||
ISIG.convert<UInt>()
|
||||
attrs.c_cc[VMIN] = 9u
|
||||
attrs.c_cc[VTIME] = 9u
|
||||
|
||||
configureRawInput(attrs)
|
||||
|
||||
assertEquals(0u, attrs.c_iflag and BRKINT.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_iflag and ICRNL.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_iflag and INPCK.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_iflag and ISTRIP.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_iflag and IXON.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_oflag and OPOST.convert<UInt>())
|
||||
assertEquals(CS8.convert<UInt>(), attrs.c_cflag and CS8.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_lflag and ECHO.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_lflag and ICANON.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_lflag and IEXTEN.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_lflag and ISIG.convert<UInt>())
|
||||
assertEquals(0u, attrs.c_cc[VMIN].toUInt())
|
||||
assertEquals(1u, attrs.c_cc[VTIME].toUInt())
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,35 @@
|
||||
|
||||
package net.sergeych.lyngio.console
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.toKString
|
||||
import platform.posix.fclose
|
||||
import platform.posix.fopen
|
||||
import platform.posix.fputs
|
||||
import platform.posix.getenv
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
private val flowDebugLogFilePath: String
|
||||
get() = getenv("LYNG_CONSOLE_DEBUG_LOG")?.toKString()?.takeIf { it.isNotBlank() }
|
||||
?: "/tmp/lyng_console_flow_debug.log"
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
internal actual fun consoleFlowDebug(message: String, error: Throwable?) {
|
||||
// no-op on Native
|
||||
runCatching {
|
||||
val line = buildString {
|
||||
append("[console-flow] ")
|
||||
append(message)
|
||||
append('\n')
|
||||
if (error != null) {
|
||||
append(error.toString())
|
||||
append('\n')
|
||||
}
|
||||
}
|
||||
val file = fopen(flowDebugLogFilePath, "ab") ?: return
|
||||
try {
|
||||
fputs(line, file)
|
||||
} finally {
|
||||
fclose(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user