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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import platform.posix.*
|
import platform.posix.*
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
|
|
||||||
internal actual fun getNativeSystemConsole(): LyngConsole = LinuxPosixLyngConsole
|
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 {
|
internal object LinuxConsoleKeyDecoder {
|
||||||
fun decode(firstCode: Int, nextCode: (Long) -> Int?): ConsoleEvent.KeyDown {
|
fun decode(firstCode: Int, nextCode: (Long) -> Int?): ConsoleEvent.KeyDown {
|
||||||
if (firstCode == 27) {
|
if (firstCode == 27) {
|
||||||
@ -132,6 +135,7 @@ object LinuxPosixLyngConsole : LyngConsole {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consoleFlowDebug("linux-events: source created")
|
||||||
return object : ConsoleEventSource {
|
return object : ConsoleEventSource {
|
||||||
var closed = false
|
var closed = false
|
||||||
var lastGeometry: ConsoleGeometry? = null
|
var lastGeometry: ConsoleGeometry? = null
|
||||||
@ -147,23 +151,27 @@ object LinuxPosixLyngConsole : LyngConsole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val rawRequested = stateMutex.withLock { rawModeRequested }
|
val rawRequested = stateMutex.withLock { rawModeRequested }
|
||||||
val pollSliceMs = if (timeoutMs <= 0L) 250L else minOf(250L, timeoutMs)
|
|
||||||
if (rawRequested) {
|
if (rawRequested) {
|
||||||
val ev = readKeyEvent(pollSliceMs)
|
val ev = readKeyEventNonBlocking()
|
||||||
if (ev != null) return ev
|
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
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun close() {
|
override suspend fun close() {
|
||||||
closed = true
|
closed = true
|
||||||
|
consoleFlowDebug("linux-events: source closed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,12 +191,11 @@ object LinuxPosixLyngConsole : LyngConsole {
|
|||||||
}
|
}
|
||||||
savedAttrsBlob = saved
|
savedAttrsBlob = saved
|
||||||
|
|
||||||
attrs.c_lflag = attrs.c_lflag and ICANON.convert<UInt>().inv() and ECHO.convert<UInt>().inv()
|
configureRawInput(attrs)
|
||||||
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()
|
|
||||||
if (tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr) != 0) return@withLock false
|
if (tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr) != 0) return@withLock false
|
||||||
}
|
}
|
||||||
rawModeRequested = true
|
rawModeRequested = true
|
||||||
|
consoleFlowDebug("linux-events: setRawMode(true): enabled")
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
val hadRaw = rawModeRequested
|
val hadRaw = rawModeRequested
|
||||||
@ -203,6 +210,7 @@ object LinuxPosixLyngConsole : LyngConsole {
|
|||||||
tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr)
|
tcsetattr(STDIN_FILENO, TCSANOW, attrs.ptr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
consoleFlowDebug("linux-events: setRawMode(false): disabled hadRaw=$hadRaw")
|
||||||
hadRaw
|
hadRaw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,19 +233,55 @@ object LinuxPosixLyngConsole : LyngConsole {
|
|||||||
val ready = poll(pfd.ptr, 1.convert(), timeoutMs.toInt())
|
val ready = poll(pfd.ptr, 1.convert(), timeoutMs.toInt())
|
||||||
if (ready <= 0) return null
|
if (ready <= 0) return null
|
||||||
|
|
||||||
|
readReadyByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readReadyByte(): Int? {
|
||||||
val buf = ByteArray(1)
|
val buf = ByteArray(1)
|
||||||
val count = buf.usePinned { pinned ->
|
val count = buf.usePinned { pinned ->
|
||||||
read(STDIN_FILENO, pinned.addressOf(0), 1.convert())
|
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()
|
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? {
|
private fun readByteNow(): Int? = memScoped {
|
||||||
val first = readByte(timeoutMs) ?: return null
|
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 ->
|
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
|
package net.sergeych.lyngio.console
|
||||||
|
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
import platform.posix.*
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
@ -64,4 +66,41 @@ class LinuxPosixLyngConsoleTest {
|
|||||||
assertEquals("A", ev.key)
|
assertEquals("A", ev.key)
|
||||||
assertTrue(ev.shift)
|
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
|
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?) {
|
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