+added console spport for lyngio
+added console support to lyng/jlyng CLI +added unicode escapes +created tetris console sample
This commit is contained in:
1 parent
1a080bc53e
commit
d9d7cafec8
44 files changed
+3842
-28
No files matched your search
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.io.console
|
||||
|
||||
import net.sergeych.lyng.ModuleScope
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjBool
|
||||
import net.sergeych.lyng.obj.ObjIterable
|
||||
import net.sergeych.lyng.obj.ObjIterationFinishedException
|
||||
import net.sergeych.lyng.obj.ObjIterator
|
||||
import net.sergeych.lyng.obj.ObjNull
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
import net.sergeych.lyng.obj.requiredArg
|
||||
import net.sergeych.lyng.obj.thisAs
|
||||
import net.sergeych.lyng.obj.toObj
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.raiseIllegalOperation
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lyngio.console.ConsoleEvent
|
||||
import net.sergeych.lyngio.console.ConsoleEventSource
|
||||
import net.sergeych.lyngio.console.LyngConsole
|
||||
import net.sergeych.lyngio.console.getSystemConsole
|
||||
import net.sergeych.lyngio.console.security.ConsoleAccessDeniedException
|
||||
import net.sergeych.lyngio.console.security.ConsoleAccessPolicy
|
||||
import net.sergeych.lyngio.console.security.LyngConsoleSecured
|
||||
|
||||
/**
|
||||
* Install Lyng module `lyng.io.console` into the given scope's ImportManager.
|
||||
*/
|
||||
fun createConsoleModule(policy: ConsoleAccessPolicy, scope: Scope): Boolean =
|
||||
createConsoleModule(policy, scope.importManager)
|
||||
|
||||
fun createConsole(policy: ConsoleAccessPolicy, scope: Scope): Boolean = createConsoleModule(policy, scope)
|
||||
|
||||
/** Same as [createConsoleModule] but with explicit [ImportManager]. */
|
||||
fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean {
|
||||
val name = "lyng.io.console"
|
||||
if (manager.packageNames.contains(name)) return false
|
||||
|
||||
manager.addPackage(name) { module ->
|
||||
buildConsoleModule(module, policy)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager)
|
||||
|
||||
private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy) {
|
||||
val console: LyngConsole = LyngConsoleSecured(getSystemConsole(), policy)
|
||||
|
||||
val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {}
|
||||
|
||||
consoleType.apply {
|
||||
addClassFnDoc(
|
||||
name = "isSupported",
|
||||
doc = "Whether console control API is supported on this platform.",
|
||||
returns = type("lyng.Bool"),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
ObjBool(console.isSupported)
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "isTty",
|
||||
doc = "Whether current stdout is attached to an interactive TTY.",
|
||||
returns = type("lyng.Bool"),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
ObjBool(console.isTty())
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "ansiLevel",
|
||||
doc = "Detected ANSI color capability: NONE, BASIC16, ANSI256, TRUECOLOR.",
|
||||
returns = type("lyng.String"),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
ObjString(console.ansiLevel().name)
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "geometry",
|
||||
doc = "Current terminal geometry or null.",
|
||||
returns = type("ConsoleGeometry", nullable = true),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
console.geometry()?.let { ObjConsoleGeometry(it.columns, it.rows) } ?: ObjNull
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "details",
|
||||
doc = "Get consolidated console details.",
|
||||
returns = type("ConsoleDetails"),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
val tty = console.isTty()
|
||||
val ansi = console.ansiLevel()
|
||||
val geometry = console.geometry()
|
||||
ObjConsoleDetails(
|
||||
supported = console.isSupported,
|
||||
isTty = tty,
|
||||
ansiLevel = ansi.name,
|
||||
geometry = geometry?.let { ObjConsoleGeometry(it.columns, it.rows) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "write",
|
||||
doc = "Write text directly to console output.",
|
||||
params = listOf(ParamDoc("text", type("lyng.String"))),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
val text = requiredArg<ObjString>(0).value
|
||||
console.write(text)
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "flush",
|
||||
doc = "Flush console output buffer.",
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
console.flush()
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "home",
|
||||
doc = "Move cursor to home position (1,1).",
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
console.write("\u001B[H")
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "clear",
|
||||
doc = "Clear the visible screen buffer.",
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
console.write("\u001B[2J")
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "moveTo",
|
||||
doc = "Move cursor to 1-based row and column.",
|
||||
params = listOf(
|
||||
ParamDoc("row", type("lyng.Int")),
|
||||
ParamDoc("column", type("lyng.Int")),
|
||||
),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
val row = requiredArg<net.sergeych.lyng.obj.ObjInt>(0).value
|
||||
val col = requiredArg<net.sergeych.lyng.obj.ObjInt>(1).value
|
||||
console.write("\u001B[${row};${col}H")
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "clearLine",
|
||||
doc = "Clear the current line.",
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
console.write("\u001B[2K")
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "enterAltScreen",
|
||||
doc = "Switch to terminal alternate screen buffer.",
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
console.write("\u001B[?1049h")
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "leaveAltScreen",
|
||||
doc = "Return from alternate screen buffer to normal screen.",
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
console.write("\u001B[?1049l")
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "setCursorVisible",
|
||||
doc = "Show or hide the terminal cursor.",
|
||||
params = listOf(ParamDoc("visible", type("lyng.Bool"))),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
val visible = requiredArg<ObjBool>(0).value
|
||||
console.write(if (visible) "\u001B[?25h" else "\u001B[?25l")
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "events",
|
||||
doc = "Endless iterable console event source (resize, keydown, keyup). Use in a loop, often inside launch.",
|
||||
returns = type("ConsoleEventStream"),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
console.events().toConsoleEventStream()
|
||||
}
|
||||
}
|
||||
|
||||
addClassFnDoc(
|
||||
name = "setRawMode",
|
||||
doc = "Enable or disable raw keyboard mode. Returns true if mode changed.",
|
||||
params = listOf(ParamDoc("enabled", type("lyng.Bool"))),
|
||||
returns = type("lyng.Bool"),
|
||||
moduleName = module.packageName
|
||||
) {
|
||||
consoleGuard {
|
||||
val enabled = requiredArg<ObjBool>(0).value
|
||||
ObjBool(console.setRawMode(enabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.addConstDoc(
|
||||
name = "Console",
|
||||
value = consoleType,
|
||||
doc = "Console runtime API.",
|
||||
type = type("Console"),
|
||||
moduleName = module.packageName
|
||||
)
|
||||
module.addConstDoc(
|
||||
name = "ConsoleGeometry",
|
||||
value = ObjConsoleGeometry.type,
|
||||
doc = "Terminal geometry.",
|
||||
type = type("lyng.Class"),
|
||||
moduleName = module.packageName
|
||||
)
|
||||
module.addConstDoc(
|
||||
name = "ConsoleDetails",
|
||||
value = ObjConsoleDetails.type,
|
||||
doc = "Consolidated console capability details.",
|
||||
type = type("lyng.Class"),
|
||||
moduleName = module.packageName
|
||||
)
|
||||
module.addConstDoc(
|
||||
name = "ConsoleEvent",
|
||||
value = ObjConsoleEvent.type,
|
||||
doc = "Base class for console events.",
|
||||
type = type("lyng.Class"),
|
||||
moduleName = module.packageName
|
||||
)
|
||||
module.addConstDoc(
|
||||
name = "ConsoleResizeEvent",
|
||||
value = ObjConsoleResizeEvent.type,
|
||||
doc = "Terminal resize event.",
|
||||
type = type("lyng.Class"),
|
||||
moduleName = module.packageName
|
||||
)
|
||||
module.addConstDoc(
|
||||
name = "ConsoleKeyEvent",
|
||||
value = ObjConsoleKeyEvent.typeObj,
|
||||
doc = "Keyboard event.",
|
||||
type = type("lyng.Class"),
|
||||
moduleName = module.packageName
|
||||
)
|
||||
module.addConstDoc(
|
||||
name = "ConsoleEventStream",
|
||||
value = ObjConsoleEventStream.type,
|
||||
doc = "Endless iterable stream of console events.",
|
||||
type = type("lyng.Class"),
|
||||
moduleName = module.packageName
|
||||
)
|
||||
}
|
||||
|
||||
private suspend inline fun ScopeFacade.consoleGuard(crossinline block: suspend () -> Obj): Obj {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: ConsoleAccessDeniedException) {
|
||||
raiseIllegalOperation(e.reasonDetail ?: "console access denied")
|
||||
} catch (e: Exception) {
|
||||
raiseIllegalOperation(e.message ?: "console error")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ConsoleEventSource.toConsoleEventStream(): ObjConsoleEventStream {
|
||||
return ObjConsoleEventStream(this)
|
||||
}
|
||||
|
||||
private class ObjConsoleEventStream(
|
||||
private val source: ConsoleEventSource,
|
||||
) : Obj() {
|
||||
override val objClass: net.sergeych.lyng.obj.ObjClass
|
||||
get() = type
|
||||
|
||||
companion object {
|
||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply {
|
||||
addFnDoc(
|
||||
name = "iterator",
|
||||
doc = "Create an iterator over incoming console events.",
|
||||
returns = type("lyng.Iterator"),
|
||||
moduleName = "lyng.io.console",
|
||||
) {
|
||||
val stream = thisAs<ObjConsoleEventStream>()
|
||||
ObjConsoleEventIterator(stream.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ObjConsoleEventIterator(
|
||||
private val source: ConsoleEventSource,
|
||||
) : Obj() {
|
||||
private var cached: Obj? = null
|
||||
private var closed = false
|
||||
|
||||
override val objClass: net.sergeych.lyng.obj.ObjClass
|
||||
get() = type
|
||||
|
||||
private suspend fun ensureCached(): Boolean {
|
||||
if (closed) return false
|
||||
if (cached != null) return true
|
||||
val event = source.nextEvent()
|
||||
if (event == null) {
|
||||
closeSource()
|
||||
return false
|
||||
}
|
||||
cached = event.toObjEvent()
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun closeSource() {
|
||||
if (closed) return
|
||||
closed = true
|
||||
source.close()
|
||||
}
|
||||
|
||||
suspend fun hasNext(): Boolean = ensureCached()
|
||||
|
||||
suspend fun next(scope: Scope): Obj {
|
||||
if (!ensureCached()) {
|
||||
scope.raiseError(ObjIterationFinishedException(scope))
|
||||
}
|
||||
val out = cached ?: scope.raiseError("console iterator internal error: missing cached event")
|
||||
cached = null
|
||||
return out
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventIterator", ObjIterator).apply {
|
||||
addFnDoc(
|
||||
name = "hasNext",
|
||||
doc = "Whether another console event is available.",
|
||||
returns = type("lyng.Bool"),
|
||||
moduleName = "lyng.io.console",
|
||||
) {
|
||||
thisAs<ObjConsoleEventIterator>().hasNext().toObj()
|
||||
}
|
||||
addFnDoc(
|
||||
name = "next",
|
||||
doc = "Return the next console event.",
|
||||
returns = type("ConsoleEvent"),
|
||||
moduleName = "lyng.io.console",
|
||||
) {
|
||||
thisAs<ObjConsoleEventIterator>().next(requireScope())
|
||||
}
|
||||
addFnDoc(
|
||||
name = "cancelIteration",
|
||||
doc = "Stop reading console events and release resources.",
|
||||
returns = type("lyng.Void"),
|
||||
moduleName = "lyng.io.console",
|
||||
) {
|
||||
thisAs<ObjConsoleEventIterator>().closeSource()
|
||||
ObjVoid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ConsoleEvent.toObjEvent(): Obj = when (this) {
|
||||
is ConsoleEvent.Resize -> ObjConsoleResizeEvent(columns, rows)
|
||||
is ConsoleEvent.KeyDown -> ObjConsoleKeyEvent(type = "keydown", key = key, code = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta)
|
||||
is ConsoleEvent.KeyUp -> ObjConsoleKeyEvent(type = "keyup", key = key, code = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta)
|
||||
}
|
||||
|
||||
private abstract class ObjConsoleEventBase(
|
||||
private val eventType: String,
|
||||
final override val objClass: net.sergeych.lyng.obj.ObjClass,
|
||||
) : Obj() {
|
||||
fun eventTypeName(): String = eventType
|
||||
}
|
||||
|
||||
private class ObjConsoleEvent : ObjConsoleEventBase("event", type) {
|
||||
companion object {
|
||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEvent").apply {
|
||||
addPropertyDoc(
|
||||
name = "type",
|
||||
doc = "Event type string: resize, keydown, keyup.",
|
||||
type = type("lyng.String"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { ObjString((this.thisObj as ObjConsoleEventBase).eventTypeName()) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ObjConsoleResizeEvent(
|
||||
val columns: Int,
|
||||
val rows: Int,
|
||||
) : ObjConsoleEventBase("resize", type) {
|
||||
companion object {
|
||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleResizeEvent", ObjConsoleEvent.type).apply {
|
||||
addPropertyDoc(
|
||||
name = "columns",
|
||||
doc = "Terminal width in character cells.",
|
||||
type = type("lyng.Int"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleResizeEvent).columns.toObj() }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "rows",
|
||||
doc = "Terminal height in character cells.",
|
||||
type = type("lyng.Int"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleResizeEvent).rows.toObj() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ObjConsoleKeyEvent(
|
||||
type: String,
|
||||
val key: String,
|
||||
val code: String?,
|
||||
val ctrl: Boolean,
|
||||
val alt: Boolean,
|
||||
val shift: Boolean,
|
||||
val meta: Boolean,
|
||||
) : ObjConsoleEventBase(type, typeObj) {
|
||||
companion object {
|
||||
val typeObj = net.sergeych.lyng.obj.ObjClass("ConsoleKeyEvent", ObjConsoleEvent.type).apply {
|
||||
addPropertyDoc(
|
||||
name = "key",
|
||||
doc = "Logical key name (e.g. ArrowLeft, a, Escape).",
|
||||
type = type("lyng.String"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { ObjString((this.thisObj as ObjConsoleKeyEvent).key) }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "code",
|
||||
doc = "Optional hardware/code identifier.",
|
||||
type = type("lyng.String", nullable = true),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = {
|
||||
val code = (this.thisObj as ObjConsoleKeyEvent).code
|
||||
code?.let(::ObjString) ?: ObjNull
|
||||
}
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "ctrl",
|
||||
doc = "Whether Ctrl modifier is pressed.",
|
||||
type = type("lyng.Bool"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleKeyEvent).ctrl.toObj() }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "alt",
|
||||
doc = "Whether Alt modifier is pressed.",
|
||||
type = type("lyng.Bool"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleKeyEvent).alt.toObj() }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "shift",
|
||||
doc = "Whether Shift modifier is pressed.",
|
||||
type = type("lyng.Bool"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleKeyEvent).shift.toObj() }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "meta",
|
||||
doc = "Whether Meta/Super modifier is pressed.",
|
||||
type = type("lyng.Bool"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleKeyEvent).meta.toObj() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ObjConsoleGeometry(
|
||||
val columns: Int,
|
||||
val rows: Int,
|
||||
) : Obj() {
|
||||
override val objClass: net.sergeych.lyng.obj.ObjClass get() = type
|
||||
|
||||
companion object {
|
||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleGeometry").apply {
|
||||
addPropertyDoc(
|
||||
name = "columns",
|
||||
doc = "Terminal width in character cells.",
|
||||
type = type("lyng.Int"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleGeometry).columns.toObj() }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "rows",
|
||||
doc = "Terminal height in character cells.",
|
||||
type = type("lyng.Int"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleGeometry).rows.toObj() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ObjConsoleDetails(
|
||||
val supported: Boolean,
|
||||
val isTty: Boolean,
|
||||
val ansiLevel: String,
|
||||
val geometry: ObjConsoleGeometry?,
|
||||
) : Obj() {
|
||||
override val objClass: net.sergeych.lyng.obj.ObjClass get() = type
|
||||
|
||||
companion object {
|
||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleDetails").apply {
|
||||
addPropertyDoc(
|
||||
name = "supported",
|
||||
doc = "Whether console API is supported.",
|
||||
type = type("lyng.Bool"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleDetails).supported.toObj() }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "isTty",
|
||||
doc = "Whether output is connected to a TTY.",
|
||||
type = type("lyng.Bool"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleDetails).isTty.toObj() }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "ansiLevel",
|
||||
doc = "Detected ANSI color capability level.",
|
||||
type = type("lyng.String"),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { ObjString((this.thisObj as ObjConsoleDetails).ansiLevel) }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "geometry",
|
||||
doc = "Current terminal geometry or null.",
|
||||
type = type("ConsoleGeometry", nullable = true),
|
||||
moduleName = "lyng.io.console",
|
||||
getter = { (this.thisObj as ObjConsoleDetails).geometry ?: ObjNull }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyngio.console
|
||||
|
||||
internal expect fun consoleFlowDebug(message: String, error: Throwable? = null)
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyngio.console
|
||||
|
||||
/**
|
||||
* ANSI color support level detected for the active console.
|
||||
*/
|
||||
enum class ConsoleAnsiLevel {
|
||||
NONE,
|
||||
BASIC16,
|
||||
ANSI256,
|
||||
TRUECOLOR,
|
||||
}
|
||||
|
||||
/**
|
||||
* Console geometry in character cells.
|
||||
*/
|
||||
data class ConsoleGeometry(
|
||||
val columns: Int,
|
||||
val rows: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Input/terminal events emitted by the console runtime.
|
||||
*/
|
||||
sealed interface ConsoleEvent {
|
||||
data class Resize(
|
||||
val columns: Int,
|
||||
val rows: Int,
|
||||
) : ConsoleEvent
|
||||
|
||||
data class KeyDown(
|
||||
val key: String,
|
||||
val code: String? = null,
|
||||
val ctrl: Boolean = false,
|
||||
val alt: Boolean = false,
|
||||
val shift: Boolean = false,
|
||||
val meta: Boolean = false,
|
||||
) : ConsoleEvent
|
||||
|
||||
data class KeyUp(
|
||||
val key: String,
|
||||
val code: String? = null,
|
||||
val ctrl: Boolean = false,
|
||||
val alt: Boolean = false,
|
||||
val shift: Boolean = false,
|
||||
val meta: Boolean = false,
|
||||
) : ConsoleEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull-based console event source.
|
||||
*
|
||||
* `nextEvent(timeoutMs)` returns:
|
||||
* - next event when available,
|
||||
* - `null` on timeout,
|
||||
* - `null` after close.
|
||||
*/
|
||||
interface ConsoleEventSource {
|
||||
suspend fun nextEvent(timeoutMs: Long = 0L): ConsoleEvent?
|
||||
|
||||
suspend fun close()
|
||||
}
|
||||
|
||||
private object EmptyConsoleEventSource : ConsoleEventSource {
|
||||
override suspend fun nextEvent(timeoutMs: Long): ConsoleEvent? = null
|
||||
|
||||
override suspend fun close() {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-independent console runtime surface.
|
||||
*/
|
||||
interface LyngConsole {
|
||||
val isSupported: Boolean
|
||||
|
||||
suspend fun isTty(): Boolean
|
||||
|
||||
suspend fun geometry(): ConsoleGeometry?
|
||||
|
||||
suspend fun ansiLevel(): ConsoleAnsiLevel
|
||||
|
||||
suspend fun write(text: String)
|
||||
|
||||
suspend fun flush()
|
||||
|
||||
fun events(): ConsoleEventSource
|
||||
|
||||
/**
|
||||
* Set terminal raw input mode. Returns true when mode was changed.
|
||||
*/
|
||||
suspend fun setRawMode(enabled: Boolean): Boolean
|
||||
}
|
||||
|
||||
object UnsupportedLyngConsole : LyngConsole {
|
||||
override val isSupported: Boolean = false
|
||||
|
||||
override suspend fun isTty(): Boolean = false
|
||||
|
||||
override suspend fun geometry(): ConsoleGeometry? = null
|
||||
|
||||
override suspend fun ansiLevel(): ConsoleAnsiLevel = ConsoleAnsiLevel.NONE
|
||||
|
||||
override suspend fun write(text: String) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override suspend fun flush() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun events(): ConsoleEventSource = EmptyConsoleEventSource
|
||||
|
||||
override suspend fun setRawMode(enabled: Boolean): Boolean = false
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyngio.console
|
||||
|
||||
import com.github.ajalt.mordant.input.KeyboardEvent
|
||||
import com.github.ajalt.mordant.input.RawModeScope
|
||||
import com.github.ajalt.mordant.input.enterRawModeOrNull
|
||||
import com.github.ajalt.mordant.rendering.AnsiLevel
|
||||
import com.github.ajalt.mordant.terminal.Terminal
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.sergeych.lyng.ScriptFlowIsNoMoreCollected
|
||||
import net.sergeych.mp_tools.globalLaunch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
/**
|
||||
* Mordant-backed console runtime implementation.
|
||||
*/
|
||||
object MordantLyngConsole : LyngConsole {
|
||||
private val terminal: Terminal? by lazy {
|
||||
runCatching { Terminal() }.getOrNull()
|
||||
}
|
||||
|
||||
private val stateMutex = Mutex()
|
||||
private var rawModeRequested: Boolean = false
|
||||
private var rawModeScope: RawModeScope? = null
|
||||
|
||||
private suspend fun forceRawModeReset(t: Terminal): Boolean = stateMutex.withLock {
|
||||
if (!rawModeRequested) return@withLock false
|
||||
runCatching { rawModeScope?.close() }
|
||||
.onFailure { consoleFlowDebug("forceRawModeReset: close failed", it) }
|
||||
rawModeScope = null
|
||||
rawModeRequested = false
|
||||
if (!t.terminalInfo.inputInteractive) return@withLock false
|
||||
val reopened = t.enterRawModeOrNull()
|
||||
rawModeScope = reopened
|
||||
rawModeRequested = reopened != null
|
||||
reopened != null
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = terminal != null
|
||||
|
||||
override suspend fun isTty(): Boolean {
|
||||
val t = terminal ?: return false
|
||||
return t.terminalInfo.outputInteractive
|
||||
}
|
||||
|
||||
override suspend fun geometry(): ConsoleGeometry? {
|
||||
val t = terminal ?: return null
|
||||
val size = t.updateSize()
|
||||
return ConsoleGeometry(size.width, size.height)
|
||||
}
|
||||
|
||||
override suspend fun ansiLevel(): ConsoleAnsiLevel {
|
||||
val t = terminal ?: return ConsoleAnsiLevel.NONE
|
||||
return when (t.terminalInfo.ansiLevel) {
|
||||
AnsiLevel.NONE -> ConsoleAnsiLevel.NONE
|
||||
AnsiLevel.ANSI16 -> ConsoleAnsiLevel.BASIC16
|
||||
AnsiLevel.ANSI256 -> ConsoleAnsiLevel.ANSI256
|
||||
AnsiLevel.TRUECOLOR -> ConsoleAnsiLevel.TRUECOLOR
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun write(text: String) {
|
||||
terminal?.rawPrint(text)
|
||||
}
|
||||
|
||||
override suspend fun flush() {
|
||||
// Mordant prints via platform streams immediately.
|
||||
}
|
||||
|
||||
override fun events(): ConsoleEventSource {
|
||||
val t = terminal ?: return object : ConsoleEventSource {
|
||||
override suspend fun nextEvent(timeoutMs: Long): ConsoleEvent? = null
|
||||
|
||||
override suspend fun close() {}
|
||||
}
|
||||
val out = Channel<ConsoleEvent>(Channel.UNLIMITED)
|
||||
val sourceState = Mutex()
|
||||
var running = true
|
||||
|
||||
globalLaunch {
|
||||
var lastWidth = t.updateSize().width
|
||||
var lastHeight = t.updateSize().height
|
||||
val startMark = TimeSource.Monotonic.markNow()
|
||||
var lastHeartbeatMark = startMark
|
||||
var loops = 0L
|
||||
var readAttempts = 0L
|
||||
var readFailures = 0L
|
||||
var keyEvents = 0L
|
||||
var resizeEvents = 0L
|
||||
var rawNullLoops = 0L
|
||||
var lastKeyMark = startMark
|
||||
var lastRawRecoveryMark = startMark
|
||||
|
||||
consoleFlowDebug("events: collector started")
|
||||
try {
|
||||
while (currentCoroutineContext().isActive && sourceState.withLock { running }) {
|
||||
loops += 1
|
||||
val currentSize = runCatching { t.updateSize() }.getOrNull()
|
||||
if (currentSize == null) {
|
||||
delay(150)
|
||||
continue
|
||||
}
|
||||
if (currentSize.width != lastWidth || currentSize.height != lastHeight) {
|
||||
out.trySend(ConsoleEvent.Resize(currentSize.width, currentSize.height))
|
||||
lastWidth = currentSize.width
|
||||
lastHeight = currentSize.height
|
||||
resizeEvents += 1
|
||||
}
|
||||
|
||||
val raw = stateMutex.withLock {
|
||||
if (!rawModeRequested) {
|
||||
null
|
||||
} else {
|
||||
// Recover raw scope lazily if it was dropped due to an earlier read failure.
|
||||
if (rawModeScope == null) {
|
||||
rawModeScope = t.enterRawModeOrNull()
|
||||
if (rawModeScope == null) {
|
||||
consoleFlowDebug("events: failed to reopen raw mode scope")
|
||||
} else {
|
||||
consoleFlowDebug("events: raw mode scope reopened")
|
||||
}
|
||||
}
|
||||
rawModeScope
|
||||
}
|
||||
}
|
||||
if (raw == null || !t.terminalInfo.inputInteractive) {
|
||||
rawNullLoops += 1
|
||||
delay(150)
|
||||
if (lastHeartbeatMark.elapsedNow() >= 2.seconds) {
|
||||
consoleFlowDebug(
|
||||
"events: heartbeat loops=$loops reads=$readAttempts readFailures=$readFailures keys=$keyEvents resize=$resizeEvents rawNullLoops=$rawNullLoops rawRequested=$rawModeRequested inputInteractive=${t.terminalInfo.inputInteractive}"
|
||||
)
|
||||
lastHeartbeatMark = TimeSource.Monotonic.markNow()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
readAttempts += 1
|
||||
val readResult = runCatching { raw.readEventOrNull(150.milliseconds) }
|
||||
if (readResult.isFailure) {
|
||||
readFailures += 1
|
||||
consoleFlowDebug("events: readEventOrNull failed; resetting raw scope", readResult.exceptionOrNull())
|
||||
// Raw scope became invalid; close and force reopen on next iteration.
|
||||
stateMutex.withLock {
|
||||
runCatching { rawModeScope?.close() }
|
||||
rawModeScope = null
|
||||
}
|
||||
delay(50)
|
||||
continue
|
||||
}
|
||||
val ev = readResult.getOrNull()
|
||||
|
||||
val resized = runCatching { t.updateSize() }.getOrNull()
|
||||
if (resized != null && (resized.width != lastWidth || resized.height != lastHeight)) {
|
||||
out.trySend(ConsoleEvent.Resize(resized.width, resized.height))
|
||||
lastWidth = resized.width
|
||||
lastHeight = resized.height
|
||||
}
|
||||
|
||||
when (ev) {
|
||||
is KeyboardEvent -> {
|
||||
keyEvents += 1
|
||||
lastKeyMark = TimeSource.Monotonic.markNow()
|
||||
out.trySend(
|
||||
ConsoleEvent.KeyDown(
|
||||
key = ev.key,
|
||||
code = null,
|
||||
ctrl = ev.ctrl,
|
||||
alt = ev.alt,
|
||||
shift = ev.shift,
|
||||
meta = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Mouse/other events are ignored in Lyng console v1.
|
||||
}
|
||||
}
|
||||
|
||||
// Some terminals silently stop delivering keyboard events while raw reads keep succeeding.
|
||||
// If we had keys before and then prolonged key inactivity, proactively recycle raw scope.
|
||||
if (keyEvents > 0L &&
|
||||
lastKeyMark.elapsedNow() >= 4.seconds &&
|
||||
lastRawRecoveryMark.elapsedNow() >= 4.seconds
|
||||
) {
|
||||
if (rawModeRequested) {
|
||||
consoleFlowDebug("events: key inactivity detected; forcing raw reset")
|
||||
val resetOk = forceRawModeReset(t)
|
||||
if (resetOk) {
|
||||
consoleFlowDebug("events: raw reset succeeded during inactivity recovery")
|
||||
lastKeyMark = TimeSource.Monotonic.markNow()
|
||||
} else {
|
||||
consoleFlowDebug("events: raw reset failed during inactivity recovery")
|
||||
}
|
||||
lastRawRecoveryMark = TimeSource.Monotonic.markNow()
|
||||
}
|
||||
}
|
||||
|
||||
if (lastHeartbeatMark.elapsedNow() >= 2.seconds) {
|
||||
consoleFlowDebug(
|
||||
"events: heartbeat loops=$loops reads=$readAttempts readFailures=$readFailures keys=$keyEvents resize=$resizeEvents rawNullLoops=$rawNullLoops rawRequested=$rawModeRequested inputInteractive=${t.terminalInfo.inputInteractive}"
|
||||
)
|
||||
lastHeartbeatMark = TimeSource.Monotonic.markNow()
|
||||
}
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
consoleFlowDebug("events: collector cancelled (normal)")
|
||||
// normal
|
||||
} catch (e: ScriptFlowIsNoMoreCollected) {
|
||||
consoleFlowDebug("events: collector stopped by flow consumer (normal)")
|
||||
// normal
|
||||
} catch (e: Exception) {
|
||||
consoleFlowDebug("events: collector loop failed", e)
|
||||
// terminate event source loop
|
||||
} finally {
|
||||
consoleFlowDebug(
|
||||
"events: collector ended uptime=${startMark.elapsedNow().inWholeMilliseconds}ms loops=$loops reads=$readAttempts readFailures=$readFailures keys=$keyEvents resize=$resizeEvents rawNullLoops=$rawNullLoops rawRequested=$rawModeRequested"
|
||||
)
|
||||
out.close()
|
||||
}
|
||||
}
|
||||
|
||||
return object : ConsoleEventSource {
|
||||
override suspend fun nextEvent(timeoutMs: Long): ConsoleEvent? {
|
||||
if (timeoutMs <= 0L) {
|
||||
return out.receiveCatching().getOrNull()
|
||||
}
|
||||
return withTimeoutOrNull(timeoutMs.milliseconds) {
|
||||
out.receiveCatching().getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
sourceState.withLock { running = false }
|
||||
out.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setRawMode(enabled: Boolean): Boolean {
|
||||
val t = terminal ?: return false
|
||||
return stateMutex.withLock {
|
||||
if (enabled) {
|
||||
if (!t.terminalInfo.inputInteractive) return@withLock false
|
||||
if (rawModeRequested) return@withLock false
|
||||
val scope = t.enterRawModeOrNull() ?: return@withLock false
|
||||
rawModeScope = scope
|
||||
rawModeRequested = true
|
||||
consoleFlowDebug("setRawMode(true): enabled")
|
||||
true
|
||||
} else {
|
||||
val hadRaw = rawModeRequested || rawModeScope != null
|
||||
rawModeRequested = false
|
||||
val scope = rawModeScope
|
||||
rawModeScope = null
|
||||
runCatching { scope?.close() }
|
||||
.onFailure { consoleFlowDebug("setRawMode(false): close failed", it) }
|
||||
consoleFlowDebug("setRawMode(false): disabled hadRaw=$hadRaw")
|
||||
hadRaw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyngio.console
|
||||
|
||||
/**
|
||||
* Get the system default console implementation.
|
||||
*/
|
||||
expect fun getSystemConsole(): LyngConsole
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyngio.console.security
|
||||
|
||||
import net.sergeych.lyngio.fs.security.AccessContext
|
||||
import net.sergeych.lyngio.fs.security.AccessDecision
|
||||
import net.sergeych.lyngio.fs.security.Decision
|
||||
|
||||
/**
|
||||
* Primitive console operations for access control decisions.
|
||||
*/
|
||||
sealed interface ConsoleAccessOp {
|
||||
data class WriteText(val length: Int) : ConsoleAccessOp
|
||||
|
||||
data object ReadEvents : ConsoleAccessOp
|
||||
|
||||
data class SetRawMode(val enabled: Boolean) : ConsoleAccessOp
|
||||
}
|
||||
|
||||
class ConsoleAccessDeniedException(
|
||||
val op: ConsoleAccessOp,
|
||||
val reasonDetail: String? = null,
|
||||
) : IllegalStateException("Console access denied for $op" + (reasonDetail?.let { ": $it" } ?: ""))
|
||||
|
||||
/**
|
||||
* Policy interface that decides whether a specific console operation is allowed.
|
||||
*/
|
||||
interface ConsoleAccessPolicy {
|
||||
suspend fun check(op: ConsoleAccessOp, ctx: AccessContext = AccessContext()): AccessDecision
|
||||
|
||||
suspend fun require(op: ConsoleAccessOp, ctx: AccessContext = AccessContext()) {
|
||||
val res = check(op, ctx)
|
||||
if (!res.isAllowed()) throw ConsoleAccessDeniedException(op, res.reason)
|
||||
}
|
||||
}
|
||||
|
||||
object PermitAllConsoleAccessPolicy : ConsoleAccessPolicy {
|
||||
override suspend fun check(op: ConsoleAccessOp, ctx: AccessContext): AccessDecision =
|
||||
AccessDecision(Decision.Allow)
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyngio.console.security
|
||||
|
||||
import net.sergeych.lyngio.console.*
|
||||
import net.sergeych.lyngio.fs.security.AccessContext
|
||||
|
||||
/**
|
||||
* Decorator that applies a [ConsoleAccessPolicy] to a delegate [LyngConsole].
|
||||
*/
|
||||
class LyngConsoleSecured(
|
||||
private val delegate: LyngConsole,
|
||||
private val policy: ConsoleAccessPolicy,
|
||||
private val ctx: AccessContext = AccessContext(),
|
||||
) : LyngConsole {
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = delegate.isSupported
|
||||
|
||||
override suspend fun isTty(): Boolean = delegate.isTty()
|
||||
|
||||
override suspend fun geometry(): ConsoleGeometry? = delegate.geometry()
|
||||
|
||||
override suspend fun ansiLevel(): ConsoleAnsiLevel = delegate.ansiLevel()
|
||||
|
||||
override suspend fun write(text: String) {
|
||||
policy.require(ConsoleAccessOp.WriteText(text.length), ctx)
|
||||
delegate.write(text)
|
||||
}
|
||||
|
||||
override suspend fun flush() {
|
||||
delegate.flush()
|
||||
}
|
||||
|
||||
override fun events(): ConsoleEventSource {
|
||||
val source = delegate.events()
|
||||
return object : ConsoleEventSource {
|
||||
override suspend fun nextEvent(timeoutMs: Long): ConsoleEvent? {
|
||||
policy.require(ConsoleAccessOp.ReadEvents, ctx)
|
||||
return source.nextEvent(timeoutMs)
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
source.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setRawMode(enabled: Boolean): Boolean {
|
||||
policy.require(ConsoleAccessOp.SetRawMode(enabled), ctx)
|
||||
return delegate.setRawMode(enabled)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyngio.docs
|
||||
|
||||
import net.sergeych.lyng.miniast.BuiltinDocRegistry
|
||||
import net.sergeych.lyng.miniast.ParamDoc
|
||||
import net.sergeych.lyng.miniast.type
|
||||
|
||||
object ConsoleBuiltinDocs {
|
||||
private var registered = false
|
||||
|
||||
fun ensure() {
|
||||
if (registered) return
|
||||
BuiltinDocRegistry.module("lyng.io.console") {
|
||||
classDoc(
|
||||
name = "Console",
|
||||
doc = "Console runtime API."
|
||||
) {
|
||||
method(
|
||||
name = "isSupported",
|
||||
doc = "Whether console control API is supported on this platform.",
|
||||
returns = type("lyng.Bool"),
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "isTty",
|
||||
doc = "Whether stdout is attached to an interactive TTY.",
|
||||
returns = type("lyng.Bool"),
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "ansiLevel",
|
||||
doc = "Detected ANSI color capability: NONE, BASIC16, ANSI256, TRUECOLOR.",
|
||||
returns = type("lyng.String"),
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "geometry",
|
||||
doc = "Current terminal geometry or null.",
|
||||
returns = type("ConsoleGeometry", nullable = true),
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "details",
|
||||
doc = "Get consolidated console details.",
|
||||
returns = type("ConsoleDetails"),
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "write",
|
||||
doc = "Write text directly to console output.",
|
||||
params = listOf(ParamDoc("text", type("lyng.String"))),
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "flush",
|
||||
doc = "Flush console output buffer.",
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "home",
|
||||
doc = "Move cursor to home position (1,1).",
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "clear",
|
||||
doc = "Clear the visible screen buffer.",
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "moveTo",
|
||||
doc = "Move cursor to 1-based row and column.",
|
||||
params = listOf(
|
||||
ParamDoc("row", type("lyng.Int")),
|
||||
ParamDoc("column", type("lyng.Int")),
|
||||
),
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "clearLine",
|
||||
doc = "Clear the current line.",
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "enterAltScreen",
|
||||
doc = "Switch to terminal alternate screen buffer.",
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "leaveAltScreen",
|
||||
doc = "Return from alternate screen buffer to normal screen.",
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "setCursorVisible",
|
||||
doc = "Show or hide the terminal cursor.",
|
||||
params = listOf(ParamDoc("visible", type("lyng.Bool"))),
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "events",
|
||||
doc = "Endless iterable console event source (resize, keydown, keyup). Use in a loop, often inside launch.",
|
||||
returns = type("ConsoleEventStream"),
|
||||
isStatic = true
|
||||
)
|
||||
method(
|
||||
name = "setRawMode",
|
||||
doc = "Enable or disable raw keyboard mode. Returns true if mode changed.",
|
||||
params = listOf(ParamDoc("enabled", type("lyng.Bool"))),
|
||||
returns = type("lyng.Bool"),
|
||||
isStatic = true
|
||||
)
|
||||
}
|
||||
classDoc(
|
||||
name = "ConsoleEventStream",
|
||||
doc = "Endless iterable stream of console events."
|
||||
) {
|
||||
method(
|
||||
name = "iterator",
|
||||
doc = "Create an iterator over incoming console events.",
|
||||
returns = type("lyng.Iterator")
|
||||
)
|
||||
}
|
||||
classDoc(
|
||||
name = "ConsoleGeometry",
|
||||
doc = "Terminal geometry."
|
||||
) {
|
||||
field(
|
||||
name = "columns",
|
||||
doc = "Terminal width in character cells.",
|
||||
type = type("lyng.Int")
|
||||
)
|
||||
field(
|
||||
name = "rows",
|
||||
doc = "Terminal height in character cells.",
|
||||
type = type("lyng.Int")
|
||||
)
|
||||
}
|
||||
classDoc(
|
||||
name = "ConsoleDetails",
|
||||
doc = "Consolidated console capability details."
|
||||
) {
|
||||
field(
|
||||
name = "supported",
|
||||
doc = "Whether console API is supported.",
|
||||
type = type("lyng.Bool")
|
||||
)
|
||||
field(
|
||||
name = "isTty",
|
||||
doc = "Whether output is attached to a TTY.",
|
||||
type = type("lyng.Bool")
|
||||
)
|
||||
field(
|
||||
name = "ansiLevel",
|
||||
doc = "Detected ANSI color capability.",
|
||||
type = type("lyng.String")
|
||||
)
|
||||
field(
|
||||
name = "geometry",
|
||||
doc = "Current geometry or null.",
|
||||
type = type("ConsoleGeometry", nullable = true)
|
||||
)
|
||||
}
|
||||
classDoc(
|
||||
name = "ConsoleEvent",
|
||||
doc = "Base class for console events."
|
||||
) {
|
||||
field(
|
||||
name = "type",
|
||||
doc = "Event kind string.",
|
||||
type = type("lyng.String")
|
||||
)
|
||||
}
|
||||
classDoc(
|
||||
name = "ConsoleResizeEvent",
|
||||
doc = "Resize event."
|
||||
) {
|
||||
field(
|
||||
name = "type",
|
||||
doc = "Event kind string: resize.",
|
||||
type = type("lyng.String")
|
||||
)
|
||||
field(
|
||||
name = "columns",
|
||||
doc = "Terminal width in character cells.",
|
||||
type = type("lyng.Int")
|
||||
)
|
||||
field(
|
||||
name = "rows",
|
||||
doc = "Terminal height in character cells.",
|
||||
type = type("lyng.Int")
|
||||
)
|
||||
}
|
||||
classDoc(
|
||||
name = "ConsoleKeyEvent",
|
||||
doc = "Keyboard event."
|
||||
) {
|
||||
field(
|
||||
name = "type",
|
||||
doc = "Event kind string: keydown or keyup.",
|
||||
type = type("lyng.String")
|
||||
)
|
||||
field(
|
||||
name = "key",
|
||||
doc = "Logical key name.",
|
||||
type = type("lyng.String")
|
||||
)
|
||||
field(
|
||||
name = "code",
|
||||
doc = "Optional hardware code.",
|
||||
type = type("lyng.String", nullable = true)
|
||||
)
|
||||
field(
|
||||
name = "ctrl",
|
||||
doc = "Ctrl modifier state.",
|
||||
type = type("lyng.Bool")
|
||||
)
|
||||
field(
|
||||
name = "alt",
|
||||
doc = "Alt modifier state.",
|
||||
type = type("lyng.Bool")
|
||||
)
|
||||
field(
|
||||
name = "shift",
|
||||
doc = "Shift modifier state.",
|
||||
type = type("lyng.Bool")
|
||||
)
|
||||
field(
|
||||
name = "meta",
|
||||
doc = "Meta modifier state.",
|
||||
type = type("lyng.Bool")
|
||||
)
|
||||
}
|
||||
|
||||
valDoc(
|
||||
name = "Console",
|
||||
doc = "Console runtime API.",
|
||||
type = type("Console")
|
||||
)
|
||||
valDoc(name = "ConsoleGeometry", doc = "Terminal geometry class.", type = type("lyng.Class"))
|
||||
valDoc(name = "ConsoleDetails", doc = "Console details class.", type = type("lyng.Class"))
|
||||
valDoc(name = "ConsoleEvent", doc = "Base console event class.", type = type("lyng.Class"))
|
||||
valDoc(name = "ConsoleResizeEvent", doc = "Resize event class.", type = type("lyng.Class"))
|
||||
valDoc(name = "ConsoleKeyEvent", doc = "Keyboard event class.", type = type("lyng.Class"))
|
||||
valDoc(name = "ConsoleEventStream", doc = "Iterable console event stream class.", type = type("lyng.Class"))
|
||||
}
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user