1.5.0-RC2: improved lyngio.console, some polishing
This commit is contained in:
parent
d92309d76c
commit
2a79c718ba
@ -5,6 +5,13 @@
|
|||||||
- For generics-heavy code generation, follow `docs/ai_language_reference.md` section `7.1 Generics Runtime Model and Bounds` and `7.2 Differences vs Java / Kotlin / Scala`.
|
- For generics-heavy code generation, follow `docs/ai_language_reference.md` section `7.1 Generics Runtime Model and Bounds` and `7.2 Differences vs Java / Kotlin / Scala`.
|
||||||
- Use `docs/ai_stdlib_reference.md` for default runtime/module APIs and stdlib surface.
|
- Use `docs/ai_stdlib_reference.md` for default runtime/module APIs and stdlib surface.
|
||||||
- Treat `LYNG_AI_SPEC.md` and older docs as secondary if they conflict with the two files above.
|
- Treat `LYNG_AI_SPEC.md` and older docs as secondary if they conflict with the two files above.
|
||||||
|
- Prefer the shortest clear loop: use `for` for straightforward iteration/ranges; use `while` only when loop state/condition is irregular or changes in ways `for` cannot express cleanly.
|
||||||
|
|
||||||
|
## Lyng-First API Declarations
|
||||||
|
- Use `.lyng` declarations as the single source of truth for Lyng-facing API docs and types (especially module extern declarations).
|
||||||
|
- Prefer defining Lyng entities (enums/classes/type shapes) in `.lyng` files; only define them in Kotlin when there is Kotlin/platform-specific implementation detail that cannot be expressed in Lyng.
|
||||||
|
- Avoid hardcoding Lyng API documentation in Kotlin registrars when it can be declared in `.lyng`; Kotlin-side docs should be fallback/bridge only.
|
||||||
|
- For mixed pluggable modules (Lyng + Kotlin), embed module `.lyng` sources as generated Kotlin string literals, evaluate them into module scope during registration, then attach Kotlin implementations/bindings.
|
||||||
|
|
||||||
## Kotlin/Wasm generation guardrails
|
## Kotlin/Wasm generation guardrails
|
||||||
- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`.
|
- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`.
|
||||||
|
|||||||
@ -12,11 +12,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import lyng.io.console
|
import lyng.io.console
|
||||||
|
import lyng.io.fs
|
||||||
|
|
||||||
val MIN_COLS = 56
|
val MIN_COLS = 56
|
||||||
val MIN_ROWS = 24
|
val MIN_ROWS = 24
|
||||||
val PANEL_WIDTH = 24
|
val PANEL_WIDTH = 24
|
||||||
val BOARD_MARGIN_ROWS = 8
|
val BOARD_MARGIN_ROWS = 5
|
||||||
val BOARD_MIN_W = 10
|
val BOARD_MIN_W = 10
|
||||||
val BOARD_MAX_W = 16
|
val BOARD_MAX_W = 16
|
||||||
val BOARD_MIN_H = 16
|
val BOARD_MIN_H = 16
|
||||||
@ -29,6 +30,7 @@ val RESIZE_WAIT_MS = 250
|
|||||||
val ROTATION_KICKS = [0, -1, 1, -2, 2]
|
val ROTATION_KICKS = [0, -1, 1, -2, 2]
|
||||||
val ANSI_ESC = "\u001b["
|
val ANSI_ESC = "\u001b["
|
||||||
val ANSI_RESET = ANSI_ESC + "0m"
|
val ANSI_RESET = ANSI_ESC + "0m"
|
||||||
|
val ERROR_LOG_PATH = "/tmp/lyng_tetris_errors.log"
|
||||||
val UNICODE_BLOCK = "██"
|
val UNICODE_BLOCK = "██"
|
||||||
val UNICODE_TOP_LEFT = "┌"
|
val UNICODE_TOP_LEFT = "┌"
|
||||||
val UNICODE_TOP_RIGHT = "┐"
|
val UNICODE_TOP_RIGHT = "┐"
|
||||||
@ -72,44 +74,37 @@ fun clearAndHome() {
|
|||||||
Console.home()
|
Console.home()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logError(message: String, err: Object?): Void {
|
||||||
|
try {
|
||||||
|
val details = if (err == null) "" else ": " + err
|
||||||
|
Path(ERROR_LOG_PATH).appendUtf8(message + details + "\n")
|
||||||
|
} catch (_: Object) {
|
||||||
|
// Never let logging errors affect gameplay.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun repeatText(s: String, n: Int): String {
|
fun repeatText(s: String, n: Int): String {
|
||||||
var out: String = ""
|
var out: String = ""
|
||||||
var i = 0
|
for (i in 0..<n) {
|
||||||
while (i < n) {
|
|
||||||
out += s
|
out += s
|
||||||
i = i + 1
|
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fun maxInt(a: Int, b: Int): Int {
|
fun max<T>(a: T, b: T): T = if (a > b) a else b
|
||||||
if (a > b) a else b
|
|
||||||
}
|
|
||||||
|
|
||||||
fun minInt(a: Int, b: Int): Int {
|
|
||||||
if (a < b) a else b
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clampInt(v: Int, lo: Int, hi: Int): Int {
|
|
||||||
minInt(hi, maxInt(lo, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun emptyRow(width: Int): Row {
|
fun emptyRow(width: Int): Row {
|
||||||
val r: Row = []
|
val r: Row = []
|
||||||
var x = 0
|
for (x in 0..<width) {
|
||||||
while (x < width) {
|
|
||||||
r.add(0)
|
r.add(0)
|
||||||
x = x + 1
|
|
||||||
}
|
}
|
||||||
r
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createBoard(width: Int, height: Int): Board {
|
fun createBoard(width: Int, height: Int): Board {
|
||||||
val b: Board = []
|
val b: Board = []
|
||||||
var y = 0
|
for (y in 0..<height) {
|
||||||
while (y < height) {
|
|
||||||
b.add(emptyRow(width))
|
b.add(emptyRow(width))
|
||||||
y = y + 1
|
|
||||||
}
|
}
|
||||||
b
|
b
|
||||||
}
|
}
|
||||||
@ -139,7 +134,9 @@ fun emptyCellText(useColor: Bool): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun canPlace(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px: Int, py: Int): Bool {
|
fun canPlace(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px: Int, py: Int): Bool {
|
||||||
|
if (pieceId < 1 || pieceId > 7) return false
|
||||||
val piece: Piece = PIECES[pieceId - 1]
|
val piece: Piece = PIECES[pieceId - 1]
|
||||||
|
if (rot < 0 || rot >= piece.rotations.size) return false
|
||||||
val cells = piece.rotations[rot]
|
val cells = piece.rotations[rot]
|
||||||
|
|
||||||
for (cell in cells) {
|
for (cell in cells) {
|
||||||
@ -180,21 +177,19 @@ fun clearCompletedLines(board: Board, boardW: Int, boardH: Int): Int {
|
|||||||
while (y >= 0) {
|
while (y >= 0) {
|
||||||
val row = b[y]
|
val row = b[y]
|
||||||
var full = true
|
var full = true
|
||||||
var x = 0
|
for (x in 0..<boardW) {
|
||||||
while (x < boardW) {
|
|
||||||
if (row[x] == 0) {
|
if (row[x] == 0) {
|
||||||
full = false
|
full = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
x = x + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (full) {
|
if (full) {
|
||||||
b.removeAt(y)
|
b.removeAt(y)
|
||||||
b.insertAt(0, emptyRow(boardW))
|
b.insertAt(0, emptyRow(boardW))
|
||||||
cleared = cleared + 1
|
cleared++
|
||||||
} else {
|
} else {
|
||||||
y = y - 1
|
y--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +215,7 @@ fun tryRotateCw(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int,
|
|||||||
val nr = (rot + 1) % rotations
|
val nr = (rot + 1) % rotations
|
||||||
for (kx in ROTATION_KICKS) {
|
for (kx in ROTATION_KICKS) {
|
||||||
val nx = px + kx
|
val nx = px + kx
|
||||||
if (canPlace(board, boardW, boardH, pieceId, nr, nx, py)) {
|
if (canPlace(board, boardW, boardH, pieceId, nr, nx, py) == true) {
|
||||||
return RotateResult(true, nr, nx)
|
return RotateResult(true, nr, nx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,21 +225,18 @@ fun tryRotateCw(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int,
|
|||||||
fun nextPreviewLines(pieceId: Int, useColor: Bool): List<String> {
|
fun nextPreviewLines(pieceId: Int, useColor: Bool): List<String> {
|
||||||
val out: List<String> = []
|
val out: List<String> = []
|
||||||
if (pieceId <= 0) {
|
if (pieceId <= 0) {
|
||||||
|
for (i in 0..<4) {
|
||||||
out.add(" ")
|
out.add(" ")
|
||||||
out.add(" ")
|
}
|
||||||
out.add(" ")
|
|
||||||
out.add(" ")
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
val piece: Piece = PIECES[pieceId - 1]
|
val piece: Piece = PIECES[pieceId - 1]
|
||||||
val cells = piece.rotations[0]
|
val cells = piece.rotations[0]
|
||||||
|
|
||||||
var y = 0
|
for (y in 0..<4) {
|
||||||
while (y < 4) {
|
|
||||||
var line = ""
|
var line = ""
|
||||||
var x = 0
|
for (x in 0..<4) {
|
||||||
while (x < 4) {
|
|
||||||
var filled = false
|
var filled = false
|
||||||
for (cell in cells) {
|
for (cell in cells) {
|
||||||
if (cell[0] == x && cell[1] == y) {
|
if (cell[0] == x && cell[1] == y) {
|
||||||
@ -253,10 +245,8 @@ fun nextPreviewLines(pieceId: Int, useColor: Bool): List<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
line += if (filled) blockText(pieceId, useColor) else " "
|
line += if (filled) blockText(pieceId, useColor) else " "
|
||||||
x = x + 1
|
|
||||||
}
|
}
|
||||||
out.add(line)
|
out.add(line)
|
||||||
y = y + 1
|
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@ -300,44 +290,36 @@ fun render(
|
|||||||
|
|
||||||
val frameLines: List<String> = []
|
val frameLines: List<String> = []
|
||||||
|
|
||||||
var y = 0
|
for (y in 0..<boardH) {
|
||||||
while (y < boardH) {
|
|
||||||
var line = UNICODE_VERTICAL
|
var line = UNICODE_VERTICAL
|
||||||
|
|
||||||
var x = 0
|
for (x in 0..<boardW) {
|
||||||
while (x < boardW) {
|
|
||||||
val a = activeCellId(state.pieceId, state.rot, state.px, state.py, x, y)
|
val a = activeCellId(state.pieceId, state.rot, state.px, state.py, x, y)
|
||||||
val row = board[y]
|
val row = board[y]
|
||||||
val b = row[x]
|
val b = row[x]
|
||||||
val id = if (a > 0) a else b
|
val id = if (a > 0) a else b
|
||||||
line += if (id > 0) blockText(id, useColor) else emptyCellText(useColor)
|
line += if (id > 0) blockText(id, useColor) else emptyCellText(useColor)
|
||||||
x = x + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
line += UNICODE_VERTICAL
|
line += UNICODE_VERTICAL
|
||||||
|
|
||||||
val p = y
|
if (y < panel.size) {
|
||||||
if (p < panel.size) {
|
line += " " + panel[y]
|
||||||
line += " " + panel[p]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frameLines.add(line)
|
frameLines.add(line)
|
||||||
y = y + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frameLines.add(bottomBorder)
|
frameLines.add(bottomBorder)
|
||||||
|
|
||||||
val prev = prevFrameLines
|
for (i in 0..<frameLines.size) {
|
||||||
var i = 0
|
|
||||||
while (i < frameLines.size) {
|
|
||||||
val line = frameLines[i]
|
val line = frameLines[i]
|
||||||
val old = if (i < prev.size) prev[i] else null
|
val old = if (i < prevFrameLines.size) prevFrameLines[i] else null
|
||||||
if (old != line) {
|
if (old != line) {
|
||||||
Console.moveTo(originRow + i, originCol)
|
Console.moveTo(originRow + i, originCol)
|
||||||
Console.clearLine()
|
Console.clearLine()
|
||||||
Console.write(line)
|
Console.write(line)
|
||||||
}
|
}
|
||||||
i = i + 1
|
|
||||||
}
|
}
|
||||||
frameLines
|
frameLines
|
||||||
}
|
}
|
||||||
@ -351,9 +333,23 @@ fun waitForMinimumSize(minCols: Int, minRows: Int): Object {
|
|||||||
if (cols >= minCols && rows >= minRows) return g
|
if (cols >= minCols && rows >= minRows) return g
|
||||||
|
|
||||||
clearAndHome()
|
clearAndHome()
|
||||||
println("Lyng Tetris needs at least %sx%s terminal size."(minCols, minRows))
|
val lines: List<String> = []
|
||||||
println("Current: %sx%s"(cols, rows))
|
lines.add("Lyng Tetris needs at least %sx%s terminal size."(minCols, minRows))
|
||||||
println("Resize the console window to continue...")
|
lines.add("Current: %sx%s"(cols, rows))
|
||||||
|
lines.add("Resize the console window to continue.")
|
||||||
|
|
||||||
|
val visibleLines = if (rows < lines.size) rows else lines.size
|
||||||
|
if (visibleLines > 0) {
|
||||||
|
val startRow = max(1, ((rows - visibleLines) / 2) + 1)
|
||||||
|
for (i in 0..<visibleLines) {
|
||||||
|
val line = lines[i]
|
||||||
|
val startCol = max(1, ((cols - line.size) / 2) + 1)
|
||||||
|
Console.moveTo(startRow + i, startCol)
|
||||||
|
Console.clearLine()
|
||||||
|
Console.write(line)
|
||||||
|
}
|
||||||
|
Console.flush()
|
||||||
|
}
|
||||||
delay(RESIZE_WAIT_MS)
|
delay(RESIZE_WAIT_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -434,8 +430,8 @@ if (!Console.isSupported()) {
|
|||||||
val cols = g0?.columns ?: MIN_COLS
|
val cols = g0?.columns ?: MIN_COLS
|
||||||
val rows = g0?.rows ?: MIN_ROWS
|
val rows = g0?.rows ?: MIN_ROWS
|
||||||
|
|
||||||
val boardW = clampInt((cols - PANEL_WIDTH) / 2, BOARD_MIN_W, BOARD_MAX_W)
|
val boardW = clamp((cols - PANEL_WIDTH) / 2, BOARD_MIN_W..BOARD_MAX_W)
|
||||||
val boardH = clampInt(rows - BOARD_MARGIN_ROWS, BOARD_MIN_H, BOARD_MAX_H)
|
val boardH = clamp(rows - BOARD_MARGIN_ROWS, BOARD_MIN_H..BOARD_MAX_H)
|
||||||
|
|
||||||
val board: Board = createBoard(boardW, boardH)
|
val board: Board = createBoard(boardW, boardH)
|
||||||
|
|
||||||
@ -461,20 +457,28 @@ if (!Console.isSupported()) {
|
|||||||
println("Use jlyng in an interactive terminal with raw input support.")
|
println("Use jlyng in an interactive terminal with raw input support.")
|
||||||
void
|
void
|
||||||
} else {
|
} else {
|
||||||
val useColor = Console.ansiLevel() != "NONE"
|
val useColor = Console.ansiLevel() != ConsoleAnsiLevel.NONE
|
||||||
Console.enterAltScreen()
|
Console.enterAltScreen()
|
||||||
Console.setCursorVisible(false)
|
Console.setCursorVisible(false)
|
||||||
clearAndHome()
|
clearAndHome()
|
||||||
|
|
||||||
|
fun resetActivePiece(s: GameState): Void {
|
||||||
|
s.pieceId = nextPieceId()
|
||||||
|
s.rot = 0
|
||||||
|
s.px = (boardW / 2) - 2
|
||||||
|
s.py = -1
|
||||||
|
}
|
||||||
|
|
||||||
fun applyKeyInput(s: GameState, key: String): Void {
|
fun applyKeyInput(s: GameState, key: String): Void {
|
||||||
|
try {
|
||||||
if (key == "__CTRL_C__" || key == "q" || key == "Q" || key == "Escape") {
|
if (key == "__CTRL_C__" || key == "q" || key == "Q" || key == "Escape") {
|
||||||
s.running = false
|
s.running = false
|
||||||
}
|
}
|
||||||
else if (key == "ArrowLeft" || key == "a" || key == "A") {
|
else if (key == "ArrowLeft" || key == "a" || key == "A") {
|
||||||
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px - 1, s.py)) s.px = s.px - 1
|
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px - 1, s.py) == true) s.px--
|
||||||
}
|
}
|
||||||
else if (key == "ArrowRight" || key == "d" || key == "D") {
|
else if (key == "ArrowRight" || key == "d" || key == "D") {
|
||||||
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px + 1, s.py)) s.px = s.px + 1
|
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px + 1, s.py) == true) s.px++
|
||||||
}
|
}
|
||||||
else if (key == "ArrowUp" || key == "w" || key == "W") {
|
else if (key == "ArrowUp" || key == "w" || key == "W") {
|
||||||
val rr: RotateResult = tryRotateCw(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py)
|
val rr: RotateResult = tryRotateCw(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py)
|
||||||
@ -484,17 +488,21 @@ if (!Console.isSupported()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (key == "ArrowDown" || key == "s" || key == "S") {
|
else if (key == "ArrowDown" || key == "s" || key == "S") {
|
||||||
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1)) {
|
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
|
||||||
s.py = s.py + 1
|
s.py++
|
||||||
s.score = s.score + 1
|
s.score++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (key == " ") {
|
else if (key == " ") {
|
||||||
while (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1)) {
|
while (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
|
||||||
s.py = s.py + 1
|
s.py++
|
||||||
s.score = s.score + 2
|
s.score += 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (inputErr: Object) {
|
||||||
|
logError("applyKeyInput recovered after error", inputErr)
|
||||||
|
resetActivePiece(s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var inputRunning = true
|
var inputRunning = true
|
||||||
@ -503,11 +511,28 @@ if (!Console.isSupported()) {
|
|||||||
try {
|
try {
|
||||||
for (ev in Console.events()) {
|
for (ev in Console.events()) {
|
||||||
if (!inputRunning) break
|
if (!inputRunning) break
|
||||||
|
|
||||||
|
// Isolate per-event failures so one bad event does not unwind the stream.
|
||||||
|
try {
|
||||||
if (ev is ConsoleKeyEvent) {
|
if (ev is ConsoleKeyEvent) {
|
||||||
val ke = ev as ConsoleKeyEvent
|
val ke = ev as ConsoleKeyEvent
|
||||||
if (ke.type == "keydown") {
|
if (ke.type == ConsoleEventType.KEY_DOWN) {
|
||||||
val key = ke.key
|
var key = ""
|
||||||
val ctrl = ke.ctrl
|
var ctrl = false
|
||||||
|
try {
|
||||||
|
key = ke.key
|
||||||
|
} catch (keyErr: Object) {
|
||||||
|
logError("Input key read error", keyErr)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ctrl = ke.ctrl == true
|
||||||
|
} catch (_: Object) {
|
||||||
|
ctrl = false
|
||||||
|
}
|
||||||
|
if (key == "") {
|
||||||
|
logError("Dropped key event with empty/null key", null)
|
||||||
|
continue
|
||||||
|
}
|
||||||
gameMutex.withLock {
|
gameMutex.withLock {
|
||||||
val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key
|
val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key
|
||||||
applyKeyInput(state, mapped)
|
applyKeyInput(state, mapped)
|
||||||
@ -519,10 +544,15 @@ if (!Console.isSupported()) {
|
|||||||
prevFrameLines = []
|
prevFrameLines = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (eventErr: Object) {
|
||||||
|
// Keep the input stream alive; report for diagnostics.
|
||||||
|
logError("Input event error", eventErr)
|
||||||
}
|
}
|
||||||
} catch (err: Exception) {
|
}
|
||||||
// Keep game alive: transient console-event failures should not force quit.
|
} catch (err: Object) {
|
||||||
|
// Recover stream-level failures by recreating event stream in next loop turn.
|
||||||
if (!inputRunning) break
|
if (!inputRunning) break
|
||||||
|
logError("Input stream recovered after error", err)
|
||||||
Console.setRawMode(true)
|
Console.setRawMode(true)
|
||||||
delay(50)
|
delay(50)
|
||||||
}
|
}
|
||||||
@ -548,8 +578,8 @@ if (!Console.isSupported()) {
|
|||||||
|
|
||||||
val contentCols = boardW * 2 + 2 + 3 + PANEL_WIDTH
|
val contentCols = boardW * 2 + 2 + 3 + PANEL_WIDTH
|
||||||
val contentRows = boardH + 1
|
val contentRows = boardH + 1
|
||||||
val requiredCols = maxInt(MIN_COLS, contentCols)
|
val requiredCols = max(MIN_COLS, contentCols)
|
||||||
val requiredRows = maxInt(MIN_ROWS, contentRows)
|
val requiredRows = max(MIN_ROWS, contentRows)
|
||||||
if (c < requiredCols || r < requiredRows) {
|
if (c < requiredCols || r < requiredRows) {
|
||||||
waitForMinimumSize(requiredCols, requiredRows)
|
waitForMinimumSize(requiredCols, requiredRows)
|
||||||
clearAndHome()
|
clearAndHome()
|
||||||
@ -557,21 +587,21 @@ if (!Console.isSupported()) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val originCol = maxInt(1, ((c - contentCols) / 2) + 1)
|
val originCol = max(1, ((c - contentCols) / 2) + 1)
|
||||||
val originRow = maxInt(1, ((r - contentRows) / 2) + 1)
|
val originRow = max(1, ((r - contentRows) / 2) + 1)
|
||||||
LoopFrame(resized, originRow, originCol)
|
LoopFrame(resized, originRow, originCol)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun advanceGravity(s: GameState, frame: Int): Int {
|
fun advanceGravity(s: GameState, frame: Int): Int {
|
||||||
s.level = 1 + (s.totalLines / LEVEL_LINES_STEP)
|
s.level = 1 + (s.totalLines / LEVEL_LINES_STEP)
|
||||||
val dropEvery = maxInt(DROP_FRAMES_MIN, DROP_FRAMES_BASE - s.level)
|
val dropEvery = max(DROP_FRAMES_MIN, DROP_FRAMES_BASE - s.level)
|
||||||
|
|
||||||
var nextFrame = frame + 1
|
var nextFrame = frame + 1
|
||||||
if (nextFrame < dropEvery) return nextFrame
|
if (nextFrame < dropEvery) return nextFrame
|
||||||
nextFrame = 0
|
nextFrame = 0
|
||||||
|
|
||||||
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1)) {
|
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
|
||||||
s.py = s.py + 1
|
s.py++
|
||||||
return nextFrame
|
return nextFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,8 +609,8 @@ if (!Console.isSupported()) {
|
|||||||
|
|
||||||
val cleared = clearCompletedLines(board, boardW, boardH)
|
val cleared = clearCompletedLines(board, boardW, boardH)
|
||||||
if (cleared > 0) {
|
if (cleared > 0) {
|
||||||
s.totalLines = s.totalLines + cleared
|
s.totalLines += cleared
|
||||||
s.score = s.score + scoreForLines(cleared, s.level)
|
s.score += scoreForLines(cleared, s.level)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.pieceId = s.nextId
|
s.pieceId = s.nextId
|
||||||
|
|||||||
@ -112,6 +112,64 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class GenerateLyngioConsoleDecls : DefaultTask() {
|
||||||
|
@get:InputFile
|
||||||
|
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||||
|
abstract val sourceFile: RegularFileProperty
|
||||||
|
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val outputDir: DirectoryProperty
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun generate() {
|
||||||
|
val targetPkg = "net.sergeych.lyngio.stdlib_included"
|
||||||
|
val pkgPath = targetPkg.replace('.', '/')
|
||||||
|
val targetDir = outputDir.get().asFile.resolve(pkgPath)
|
||||||
|
targetDir.mkdirs()
|
||||||
|
|
||||||
|
val text = sourceFile.get().asFile.readText()
|
||||||
|
fun escapeForQuoted(s: String): String = buildString {
|
||||||
|
for (ch in s) when (ch) {
|
||||||
|
'\\' -> append("\\\\")
|
||||||
|
'"' -> append("\\\"")
|
||||||
|
'\n' -> append("\\n")
|
||||||
|
'\r' -> {}
|
||||||
|
'\t' -> append("\\t")
|
||||||
|
else -> append(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val out = buildString {
|
||||||
|
append("package ").append(targetPkg).append("\n\n")
|
||||||
|
append("@Suppress(\"Unused\", \"MemberVisibilityCanBePrivate\")\n")
|
||||||
|
append("internal val consoleLyng = \"")
|
||||||
|
append(escapeForQuoted(text))
|
||||||
|
append("\"\n")
|
||||||
|
}
|
||||||
|
targetDir.resolve("console_types_lyng.generated.kt").writeText(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lyngioConsoleDeclsFile = layout.projectDirectory.file("stdlib/lyng/io/console.lyng")
|
||||||
|
val generatedLyngioDeclsDir = layout.buildDirectory.dir("generated/source/lyngioDecls/commonMain/kotlin")
|
||||||
|
|
||||||
|
val generateLyngioConsoleDecls by tasks.registering(GenerateLyngioConsoleDecls::class) {
|
||||||
|
sourceFile.set(lyngioConsoleDeclsFile)
|
||||||
|
outputDir.set(generatedLyngioDeclsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin.sourceSets.named("commonMain") {
|
||||||
|
kotlin.srcDir(generatedLyngioDeclsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin.targets.configureEach {
|
||||||
|
compilations.configureEach {
|
||||||
|
compileTaskProvider.configure {
|
||||||
|
dependsOn(generateLyngioConsoleDecls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "net.sergeych.lyngio"
|
namespace = "net.sergeych.lyngio"
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||||
|
|||||||
@ -20,9 +20,11 @@ package net.sergeych.lyng.io.console
|
|||||||
import net.sergeych.lyng.ModuleScope
|
import net.sergeych.lyng.ModuleScope
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.ScopeFacade
|
import net.sergeych.lyng.ScopeFacade
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.Source
|
||||||
import net.sergeych.lyng.obj.Obj
|
import net.sergeych.lyng.obj.Obj
|
||||||
import net.sergeych.lyng.obj.ObjBool
|
import net.sergeych.lyng.obj.ObjBool
|
||||||
|
import net.sergeych.lyng.obj.ObjEnumClass
|
||||||
|
import net.sergeych.lyng.obj.ObjEnumEntry
|
||||||
import net.sergeych.lyng.obj.ObjIterable
|
import net.sergeych.lyng.obj.ObjIterable
|
||||||
import net.sergeych.lyng.obj.ObjIterationFinishedException
|
import net.sergeych.lyng.obj.ObjIterationFinishedException
|
||||||
import net.sergeych.lyng.obj.ObjIterator
|
import net.sergeych.lyng.obj.ObjIterator
|
||||||
@ -42,6 +44,9 @@ import net.sergeych.lyngio.console.getSystemConsole
|
|||||||
import net.sergeych.lyngio.console.security.ConsoleAccessDeniedException
|
import net.sergeych.lyngio.console.security.ConsoleAccessDeniedException
|
||||||
import net.sergeych.lyngio.console.security.ConsoleAccessPolicy
|
import net.sergeych.lyngio.console.security.ConsoleAccessPolicy
|
||||||
import net.sergeych.lyngio.console.security.LyngConsoleSecured
|
import net.sergeych.lyngio.console.security.LyngConsoleSecured
|
||||||
|
import net.sergeych.lyngio.stdlib_included.consoleLyng
|
||||||
|
|
||||||
|
private const val CONSOLE_MODULE_NAME = "lyng.io.console"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install Lyng module `lyng.io.console` into the given scope's ImportManager.
|
* Install Lyng module `lyng.io.console` into the given scope's ImportManager.
|
||||||
@ -53,10 +58,9 @@ fun createConsole(policy: ConsoleAccessPolicy, scope: Scope): Boolean = createCo
|
|||||||
|
|
||||||
/** Same as [createConsoleModule] but with explicit [ImportManager]. */
|
/** Same as [createConsoleModule] but with explicit [ImportManager]. */
|
||||||
fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean {
|
fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean {
|
||||||
val name = "lyng.io.console"
|
if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false
|
||||||
if (manager.packageNames.contains(name)) return false
|
|
||||||
|
|
||||||
manager.addPackage(name) { module ->
|
manager.addPackage(CONSOLE_MODULE_NAME) { module ->
|
||||||
buildConsoleModule(module, policy)
|
buildConsoleModule(module, policy)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -65,59 +69,37 @@ fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Bo
|
|||||||
fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager)
|
fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager)
|
||||||
|
|
||||||
private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy) {
|
private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy) {
|
||||||
|
// Load Lyng declarations for console enums/types first (module-local source of truth).
|
||||||
|
module.eval(Source(CONSOLE_MODULE_NAME, consoleLyng))
|
||||||
|
ConsoleEnums.initialize(module)
|
||||||
val console: LyngConsole = LyngConsoleSecured(getSystemConsole(), policy)
|
val console: LyngConsole = LyngConsoleSecured(getSystemConsole(), policy)
|
||||||
|
|
||||||
val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {}
|
val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {}
|
||||||
|
|
||||||
consoleType.apply {
|
consoleType.apply {
|
||||||
addClassFnDoc(
|
addClassFn("isSupported") {
|
||||||
name = "isSupported",
|
|
||||||
doc = "Whether console control API is supported on this platform.",
|
|
||||||
returns = type("lyng.Bool"),
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
ObjBool(console.isSupported)
|
ObjBool(console.isSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("isTty") {
|
||||||
name = "isTty",
|
|
||||||
doc = "Whether current stdout is attached to an interactive TTY.",
|
|
||||||
returns = type("lyng.Bool"),
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
ObjBool(console.isTty())
|
ObjBool(console.isTty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("ansiLevel") {
|
||||||
name = "ansiLevel",
|
|
||||||
doc = "Detected ANSI color capability: NONE, BASIC16, ANSI256, TRUECOLOR.",
|
|
||||||
returns = type("lyng.String"),
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
ObjString(console.ansiLevel().name)
|
ConsoleEnums.ansiLevel(console.ansiLevel().name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("geometry") {
|
||||||
name = "geometry",
|
|
||||||
doc = "Current terminal geometry or null.",
|
|
||||||
returns = type("ConsoleGeometry", nullable = true),
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
console.geometry()?.let { ObjConsoleGeometry(it.columns, it.rows) } ?: ObjNull
|
console.geometry()?.let { ObjConsoleGeometry(it.columns, it.rows) } ?: ObjNull
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("details") {
|
||||||
name = "details",
|
|
||||||
doc = "Get consolidated console details.",
|
|
||||||
returns = type("ConsoleDetails"),
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
val tty = console.isTty()
|
val tty = console.isTty()
|
||||||
val ansi = console.ansiLevel()
|
val ansi = console.ansiLevel()
|
||||||
@ -125,18 +107,13 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
|
|||||||
ObjConsoleDetails(
|
ObjConsoleDetails(
|
||||||
supported = console.isSupported,
|
supported = console.isSupported,
|
||||||
isTty = tty,
|
isTty = tty,
|
||||||
ansiLevel = ansi.name,
|
ansiLevel = ConsoleEnums.ansiLevel(ansi.name),
|
||||||
geometry = geometry?.let { ObjConsoleGeometry(it.columns, it.rows) },
|
geometry = geometry?.let { ObjConsoleGeometry(it.columns, it.rows) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("write") {
|
||||||
name = "write",
|
|
||||||
doc = "Write text directly to console output.",
|
|
||||||
params = listOf(ParamDoc("text", type("lyng.String"))),
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
val text = requiredArg<ObjString>(0).value
|
val text = requiredArg<ObjString>(0).value
|
||||||
console.write(text)
|
console.write(text)
|
||||||
@ -144,48 +121,28 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("flush") {
|
||||||
name = "flush",
|
|
||||||
doc = "Flush console output buffer.",
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
console.flush()
|
console.flush()
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("home") {
|
||||||
name = "home",
|
|
||||||
doc = "Move cursor to home position (1,1).",
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
console.write("\u001B[H")
|
console.write("\u001B[H")
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("clear") {
|
||||||
name = "clear",
|
|
||||||
doc = "Clear the visible screen buffer.",
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
console.write("\u001B[2J")
|
console.write("\u001B[2J")
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("moveTo") {
|
||||||
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 {
|
consoleGuard {
|
||||||
val row = requiredArg<net.sergeych.lyng.obj.ObjInt>(0).value
|
val row = requiredArg<net.sergeych.lyng.obj.ObjInt>(0).value
|
||||||
val col = requiredArg<net.sergeych.lyng.obj.ObjInt>(1).value
|
val col = requiredArg<net.sergeych.lyng.obj.ObjInt>(1).value
|
||||||
@ -194,45 +151,28 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("clearLine") {
|
||||||
name = "clearLine",
|
|
||||||
doc = "Clear the current line.",
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
console.write("\u001B[2K")
|
console.write("\u001B[2K")
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("enterAltScreen") {
|
||||||
name = "enterAltScreen",
|
|
||||||
doc = "Switch to terminal alternate screen buffer.",
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
console.write("\u001B[?1049h")
|
console.write("\u001B[?1049h")
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("leaveAltScreen") {
|
||||||
name = "leaveAltScreen",
|
|
||||||
doc = "Return from alternate screen buffer to normal screen.",
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
console.write("\u001B[?1049l")
|
console.write("\u001B[?1049l")
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("setCursorVisible") {
|
||||||
name = "setCursorVisible",
|
|
||||||
doc = "Show or hide the terminal cursor.",
|
|
||||||
params = listOf(ParamDoc("visible", type("lyng.Bool"))),
|
|
||||||
moduleName = module.packageName
|
|
||||||
) {
|
|
||||||
consoleGuard {
|
consoleGuard {
|
||||||
val visible = requiredArg<ObjBool>(0).value
|
val visible = requiredArg<ObjBool>(0).value
|
||||||
console.write(if (visible) "\u001B[?25h" else "\u001B[?25l")
|
console.write(if (visible) "\u001B[?25h" else "\u001B[?25l")
|
||||||
@ -240,24 +180,13 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("events") {
|
||||||
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 {
|
consoleGuard {
|
||||||
console.events().toConsoleEventStream()
|
console.events().toConsoleEventStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addClassFnDoc(
|
addClassFn("setRawMode") {
|
||||||
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 {
|
consoleGuard {
|
||||||
val enabled = requiredArg<ObjBool>(0).value
|
val enabled = requiredArg<ObjBool>(0).value
|
||||||
ObjBool(console.setRawMode(enabled))
|
ObjBool(console.setRawMode(enabled))
|
||||||
@ -265,55 +194,13 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.addConstDoc(
|
module.addConst("Console", consoleType)
|
||||||
name = "Console",
|
module.addConst("ConsoleGeometry", ObjConsoleGeometry.type)
|
||||||
value = consoleType,
|
module.addConst("ConsoleDetails", ObjConsoleDetails.type)
|
||||||
doc = "Console runtime API.",
|
module.addConst("ConsoleEvent", ObjConsoleEvent.type)
|
||||||
type = type("Console"),
|
module.addConst("ConsoleResizeEvent", ObjConsoleResizeEvent.type)
|
||||||
moduleName = module.packageName
|
module.addConst("ConsoleKeyEvent", ObjConsoleKeyEvent.typeObj)
|
||||||
)
|
module.addConst("ConsoleEventStream", ObjConsoleEventStream.type)
|
||||||
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 {
|
private suspend inline fun ScopeFacade.consoleGuard(crossinline block: suspend () -> Obj): Obj {
|
||||||
@ -338,12 +225,7 @@ private class ObjConsoleEventStream(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply {
|
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply {
|
||||||
addFnDoc(
|
addFn("iterator") {
|
||||||
name = "iterator",
|
|
||||||
doc = "Create an iterator over incoming console events.",
|
|
||||||
returns = type("lyng.Iterator"),
|
|
||||||
moduleName = "lyng.io.console",
|
|
||||||
) {
|
|
||||||
val stream = thisAs<ObjConsoleEventStream>()
|
val stream = thisAs<ObjConsoleEventStream>()
|
||||||
ObjConsoleEventIterator(stream.source)
|
ObjConsoleEventIterator(stream.source)
|
||||||
}
|
}
|
||||||
@ -375,7 +257,10 @@ private class ObjConsoleEventIterator(
|
|||||||
private suspend fun closeSource() {
|
private suspend fun closeSource() {
|
||||||
if (closed) return
|
if (closed) return
|
||||||
closed = true
|
closed = true
|
||||||
source.close()
|
// Do not close the underlying console source from VM iterator cancellation.
|
||||||
|
// CmdFrame.cancelIterators() may call cancelIteration() while user code is still
|
||||||
|
// expected to keep processing input (e.g. recover from app-level exceptions).
|
||||||
|
// The source lifecycle is managed by the console runtime.
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun hasNext(): Boolean = ensureCached()
|
suspend fun hasNext(): Boolean = ensureCached()
|
||||||
@ -391,28 +276,13 @@ private class ObjConsoleEventIterator(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventIterator", ObjIterator).apply {
|
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventIterator", ObjIterator).apply {
|
||||||
addFnDoc(
|
addFn("hasNext") {
|
||||||
name = "hasNext",
|
|
||||||
doc = "Whether another console event is available.",
|
|
||||||
returns = type("lyng.Bool"),
|
|
||||||
moduleName = "lyng.io.console",
|
|
||||||
) {
|
|
||||||
thisAs<ObjConsoleEventIterator>().hasNext().toObj()
|
thisAs<ObjConsoleEventIterator>().hasNext().toObj()
|
||||||
}
|
}
|
||||||
addFnDoc(
|
addFn("next") {
|
||||||
name = "next",
|
|
||||||
doc = "Return the next console event.",
|
|
||||||
returns = type("ConsoleEvent"),
|
|
||||||
moduleName = "lyng.io.console",
|
|
||||||
) {
|
|
||||||
thisAs<ObjConsoleEventIterator>().next(requireScope())
|
thisAs<ObjConsoleEventIterator>().next(requireScope())
|
||||||
}
|
}
|
||||||
addFnDoc(
|
addFn("cancelIteration") {
|
||||||
name = "cancelIteration",
|
|
||||||
doc = "Stop reading console events and release resources.",
|
|
||||||
returns = type("lyng.Void"),
|
|
||||||
moduleName = "lyng.io.console",
|
|
||||||
) {
|
|
||||||
thisAs<ObjConsoleEventIterator>().closeSource()
|
thisAs<ObjConsoleEventIterator>().closeSource()
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
@ -422,27 +292,113 @@ private class ObjConsoleEventIterator(
|
|||||||
|
|
||||||
private fun ConsoleEvent.toObjEvent(): Obj = when (this) {
|
private fun ConsoleEvent.toObjEvent(): Obj = when (this) {
|
||||||
is ConsoleEvent.Resize -> ObjConsoleResizeEvent(columns, rows)
|
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.KeyDown -> ObjConsoleKeyEvent(type = ConsoleEnums.KEY_DOWN, key = key, codeName = 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)
|
is ConsoleEvent.KeyUp -> ObjConsoleKeyEvent(type = ConsoleEnums.KEY_UP, key = key, codeName = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object ConsoleEnums {
|
||||||
|
lateinit var eventTypeClass: ObjEnumClass
|
||||||
|
private set
|
||||||
|
lateinit var keyCodeClass: ObjEnumClass
|
||||||
|
private set
|
||||||
|
lateinit var ansiLevelClass: ObjEnumClass
|
||||||
|
private set
|
||||||
|
|
||||||
|
private lateinit var eventEntries: Map<String, ObjEnumEntry>
|
||||||
|
private lateinit var keyCodeEntries: Map<String, ObjEnumEntry>
|
||||||
|
private lateinit var ansiLevelEntries: Map<String, ObjEnumEntry>
|
||||||
|
|
||||||
|
val UNKNOWN: ObjEnumEntry get() = event("UNKNOWN")
|
||||||
|
val RESIZE: ObjEnumEntry get() = event("RESIZE")
|
||||||
|
val KEY_DOWN: ObjEnumEntry get() = event("KEY_DOWN")
|
||||||
|
val KEY_UP: ObjEnumEntry get() = event("KEY_UP")
|
||||||
|
val CODE_UNKNOWN: ObjEnumEntry get() = code("UNKNOWN")
|
||||||
|
val CHARACTER: ObjEnumEntry get() = code("CHARACTER")
|
||||||
|
|
||||||
|
fun initialize(module: ModuleScope) {
|
||||||
|
eventTypeClass = resolveEnum(module, "ConsoleEventType")
|
||||||
|
keyCodeClass = resolveEnum(module, "ConsoleKeyCode")
|
||||||
|
ansiLevelClass = resolveEnum(module, "ConsoleAnsiLevel")
|
||||||
|
eventEntries = resolveEntries(
|
||||||
|
eventTypeClass,
|
||||||
|
listOf("UNKNOWN", "RESIZE", "KEY_DOWN", "KEY_UP")
|
||||||
|
)
|
||||||
|
keyCodeEntries = resolveEntries(
|
||||||
|
keyCodeClass,
|
||||||
|
listOf(
|
||||||
|
"UNKNOWN", "CHARACTER", "ARROW_UP", "ARROW_DOWN", "ARROW_LEFT", "ARROW_RIGHT",
|
||||||
|
"HOME", "END", "INSERT", "DELETE", "PAGE_UP", "PAGE_DOWN",
|
||||||
|
"ESCAPE", "ENTER", "TAB", "BACKSPACE", "SPACE"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ansiLevelEntries = resolveEntries(
|
||||||
|
ansiLevelClass,
|
||||||
|
listOf("NONE", "BASIC16", "ANSI256", "TRUECOLOR")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveEnum(module: ModuleScope, enumName: String): ObjEnumClass {
|
||||||
|
val local = module.get(enumName)?.value as? ObjEnumClass
|
||||||
|
if (local != null) return local
|
||||||
|
val root = module.importProvider.rootScope.get(enumName)?.value as? ObjEnumClass
|
||||||
|
return root ?: error("lyng.io.console declaration enum is missing: $enumName")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveEntries(enumClass: ObjEnumClass, names: List<String>): Map<String, ObjEnumEntry> {
|
||||||
|
return names.associateWith { name ->
|
||||||
|
(enumClass.byName[ObjString(name)] as? ObjEnumEntry)
|
||||||
|
?: error("lyng.io.console enum entry is missing: ${enumClass.className}.$name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun event(name: String): ObjEnumEntry = eventEntries[name]
|
||||||
|
?: error("lyng.io.console enum entry is missing: ${eventTypeClass.className}.$name")
|
||||||
|
|
||||||
|
fun code(name: String): ObjEnumEntry = keyCodeEntries[name]
|
||||||
|
?: error("lyng.io.console enum entry is missing: ${keyCodeClass.className}.$name")
|
||||||
|
|
||||||
|
fun ansiLevel(name: String): ObjEnumEntry = ansiLevelEntries[name]
|
||||||
|
?: error("lyng.io.console enum entry is missing: ${ansiLevelClass.className}.$name")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val KEY_CODE_BY_KEY_NAME = mapOf(
|
||||||
|
"ArrowUp" to "ARROW_UP",
|
||||||
|
"ArrowDown" to "ARROW_DOWN",
|
||||||
|
"ArrowLeft" to "ARROW_LEFT",
|
||||||
|
"ArrowRight" to "ARROW_RIGHT",
|
||||||
|
"Home" to "HOME",
|
||||||
|
"End" to "END",
|
||||||
|
"Insert" to "INSERT",
|
||||||
|
"Delete" to "DELETE",
|
||||||
|
"PageUp" to "PAGE_UP",
|
||||||
|
"PageDown" to "PAGE_DOWN",
|
||||||
|
"Escape" to "ESCAPE",
|
||||||
|
"Enter" to "ENTER",
|
||||||
|
"Tab" to "TAB",
|
||||||
|
"Backspace" to "BACKSPACE",
|
||||||
|
" " to "SPACE",
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun codeFrom(key: String, codeName: String?): ObjEnumEntry {
|
||||||
|
val resolved = KEY_CODE_BY_KEY_NAME[codeName ?: key]
|
||||||
|
return when {
|
||||||
|
resolved != null -> ConsoleEnums.code(resolved)
|
||||||
|
key.length == 1 -> ConsoleEnums.CHARACTER
|
||||||
|
else -> ConsoleEnums.CODE_UNKNOWN
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract class ObjConsoleEventBase(
|
private abstract class ObjConsoleEventBase(
|
||||||
private val eventType: String,
|
private val type: ObjEnumEntry,
|
||||||
final override val objClass: net.sergeych.lyng.obj.ObjClass,
|
final override val objClass: net.sergeych.lyng.obj.ObjClass,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
fun eventTypeName(): String = eventType
|
fun type(): ObjEnumEntry = type
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ObjConsoleEvent : ObjConsoleEventBase("event", type) {
|
private class ObjConsoleEvent : ObjConsoleEventBase(ConsoleEnums.UNKNOWN, type) {
|
||||||
companion object {
|
companion object {
|
||||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEvent").apply {
|
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEvent").apply {
|
||||||
addPropertyDoc(
|
addProperty(name = "type", getter = { (this.thisObj as ObjConsoleEventBase).type() })
|
||||||
name = "type",
|
|
||||||
doc = "Event type string: resize, keydown, keyup.",
|
|
||||||
type = type("lyng.String"),
|
|
||||||
moduleName = "lyng.io.console",
|
|
||||||
getter = { ObjString((this.thisObj as ObjConsoleEventBase).eventTypeName()) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -450,83 +406,40 @@ private class ObjConsoleEvent : ObjConsoleEventBase("event", type) {
|
|||||||
private class ObjConsoleResizeEvent(
|
private class ObjConsoleResizeEvent(
|
||||||
val columns: Int,
|
val columns: Int,
|
||||||
val rows: Int,
|
val rows: Int,
|
||||||
) : ObjConsoleEventBase("resize", type) {
|
) : ObjConsoleEventBase(ConsoleEnums.RESIZE, type) {
|
||||||
companion object {
|
companion object {
|
||||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleResizeEvent", ObjConsoleEvent.type).apply {
|
val type = net.sergeych.lyng.obj.ObjClass("ConsoleResizeEvent", ObjConsoleEvent.type).apply {
|
||||||
addPropertyDoc(
|
addProperty(name = "columns", getter = { (this.thisObj as ObjConsoleResizeEvent).columns.toObj() })
|
||||||
name = "columns",
|
addProperty(name = "rows", getter = { (this.thisObj as ObjConsoleResizeEvent).rows.toObj() })
|
||||||
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(
|
private class ObjConsoleKeyEvent(
|
||||||
type: String,
|
type: ObjEnumEntry,
|
||||||
val key: String,
|
val key: String,
|
||||||
val code: String?,
|
val codeName: String?,
|
||||||
val ctrl: Boolean,
|
val ctrl: Boolean,
|
||||||
val alt: Boolean,
|
val alt: Boolean,
|
||||||
val shift: Boolean,
|
val shift: Boolean,
|
||||||
val meta: Boolean,
|
val meta: Boolean,
|
||||||
) : ObjConsoleEventBase(type, typeObj) {
|
) : ObjConsoleEventBase(type, typeObj) {
|
||||||
|
init {
|
||||||
|
require(key.isNotEmpty()) { "ConsoleKeyEvent.key must never be empty" }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val typeObj = net.sergeych.lyng.obj.ObjClass("ConsoleKeyEvent", ObjConsoleEvent.type).apply {
|
val typeObj = net.sergeych.lyng.obj.ObjClass("ConsoleKeyEvent", ObjConsoleEvent.type).apply {
|
||||||
addPropertyDoc(
|
addProperty(name = "key", getter = { ObjString((this.thisObj as ObjConsoleKeyEvent).key) })
|
||||||
name = "key",
|
addProperty(name = "code", getter = { codeFrom((this.thisObj as ObjConsoleKeyEvent).key, (this.thisObj as ObjConsoleKeyEvent).codeName) })
|
||||||
doc = "Logical key name (e.g. ArrowLeft, a, Escape).",
|
addProperty(name = "codeName", getter = {
|
||||||
type = type("lyng.String"),
|
val code = (this.thisObj as ObjConsoleKeyEvent).codeName
|
||||||
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
|
code?.let(::ObjString) ?: ObjNull
|
||||||
}
|
})
|
||||||
)
|
addProperty(name = "ctrl", getter = { (this.thisObj as ObjConsoleKeyEvent).ctrl.toObj() })
|
||||||
addPropertyDoc(
|
addProperty(name = "alt", getter = { (this.thisObj as ObjConsoleKeyEvent).alt.toObj() })
|
||||||
name = "ctrl",
|
addProperty(name = "shift", getter = { (this.thisObj as ObjConsoleKeyEvent).shift.toObj() })
|
||||||
doc = "Whether Ctrl modifier is pressed.",
|
addProperty(name = "meta", getter = { (this.thisObj as ObjConsoleKeyEvent).meta.toObj() })
|
||||||
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() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -539,20 +452,8 @@ private class ObjConsoleGeometry(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleGeometry").apply {
|
val type = net.sergeych.lyng.obj.ObjClass("ConsoleGeometry").apply {
|
||||||
addPropertyDoc(
|
addProperty(name = "columns", getter = { (this.thisObj as ObjConsoleGeometry).columns.toObj() })
|
||||||
name = "columns",
|
addProperty(name = "rows", getter = { (this.thisObj as ObjConsoleGeometry).rows.toObj() })
|
||||||
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() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -560,41 +461,17 @@ private class ObjConsoleGeometry(
|
|||||||
private class ObjConsoleDetails(
|
private class ObjConsoleDetails(
|
||||||
val supported: Boolean,
|
val supported: Boolean,
|
||||||
val isTty: Boolean,
|
val isTty: Boolean,
|
||||||
val ansiLevel: String,
|
val ansiLevel: ObjEnumEntry,
|
||||||
val geometry: ObjConsoleGeometry?,
|
val geometry: ObjConsoleGeometry?,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
override val objClass: net.sergeych.lyng.obj.ObjClass get() = type
|
override val objClass: net.sergeych.lyng.obj.ObjClass get() = type
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val type = net.sergeych.lyng.obj.ObjClass("ConsoleDetails").apply {
|
val type = net.sergeych.lyng.obj.ObjClass("ConsoleDetails").apply {
|
||||||
addPropertyDoc(
|
addProperty(name = "supported", getter = { (this.thisObj as ObjConsoleDetails).supported.toObj() })
|
||||||
name = "supported",
|
addProperty(name = "isTty", getter = { (this.thisObj as ObjConsoleDetails).isTty.toObj() })
|
||||||
doc = "Whether console API is supported.",
|
addProperty(name = "ansiLevel", getter = { (this.thisObj as ObjConsoleDetails).ansiLevel })
|
||||||
type = type("lyng.Bool"),
|
addProperty(name = "geometry", getter = { (this.thisObj as ObjConsoleDetails).geometry ?: ObjNull })
|
||||||
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 }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,248 +17,12 @@
|
|||||||
|
|
||||||
package net.sergeych.lyngio.docs
|
package net.sergeych.lyngio.docs
|
||||||
|
|
||||||
import net.sergeych.lyng.miniast.BuiltinDocRegistry
|
/**
|
||||||
import net.sergeych.lyng.miniast.ParamDoc
|
* Console docs are declared in `lyngio/stdlib/lyng/io/console.lyng`.
|
||||||
import net.sergeych.lyng.miniast.type
|
* Keep this shim for compatibility with reflective loaders.
|
||||||
|
*/
|
||||||
object ConsoleBuiltinDocs {
|
object ConsoleBuiltinDocs {
|
||||||
private var registered = false
|
|
||||||
|
|
||||||
fun ensure() {
|
fun ensure() {
|
||||||
if (registered) return
|
// No Kotlin-side doc registration: console.lyng is the source of truth.
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import org.jline.terminal.Terminal
|
|||||||
import org.jline.terminal.TerminalBuilder
|
import org.jline.terminal.TerminalBuilder
|
||||||
import org.jline.utils.NonBlockingReader
|
import org.jline.utils.NonBlockingReader
|
||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
|
import java.io.InterruptedIOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
@ -41,7 +42,7 @@ import kotlin.time.TimeSource
|
|||||||
* to avoid dual-terminal contention.
|
* to avoid dual-terminal contention.
|
||||||
*/
|
*/
|
||||||
object JvmLyngConsole : LyngConsole {
|
object JvmLyngConsole : LyngConsole {
|
||||||
private const val DEBUG_REVISION = "jline-r26-force-rebuild-on-noop-recovery-2026-03-19"
|
private const val DEBUG_REVISION = "jline-r27-no-close-on-vm-iterator-cancel-2026-03-19"
|
||||||
private val codeSourceLocation: String by lazy {
|
private val codeSourceLocation: String by lazy {
|
||||||
runCatching {
|
runCatching {
|
||||||
JvmLyngConsole::class.java.protectionDomain?.codeSource?.location?.toString()
|
JvmLyngConsole::class.java.protectionDomain?.codeSource?.location?.toString()
|
||||||
@ -50,6 +51,44 @@ object JvmLyngConsole : LyngConsole {
|
|||||||
|
|
||||||
private val terminalRef = AtomicReference<Terminal?>(null)
|
private val terminalRef = AtomicReference<Terminal?>(null)
|
||||||
private val terminalInitLock = Any()
|
private val terminalInitLock = Any()
|
||||||
|
private val shutdownHook = Thread(
|
||||||
|
{
|
||||||
|
restoreTerminalStateOnShutdown()
|
||||||
|
},
|
||||||
|
"lyng-console-shutdown"
|
||||||
|
).apply { isDaemon = true }
|
||||||
|
|
||||||
|
init {
|
||||||
|
runCatching { Runtime.getRuntime().addShutdownHook(shutdownHook) }
|
||||||
|
.onFailure { consoleFlowDebug("jline-events: shutdown hook install failed", it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreTerminalStateOnShutdown() {
|
||||||
|
val term = terminalRef.get() ?: return
|
||||||
|
runCatching {
|
||||||
|
term.writer().print("\u001B[?25h")
|
||||||
|
term.writer().print("\u001B[?1049l")
|
||||||
|
term.writer().flush()
|
||||||
|
}.onFailure {
|
||||||
|
consoleFlowDebug("jline-events: shutdown visual restore failed", it)
|
||||||
|
}
|
||||||
|
val saved = if (runCatching { stateMutex.tryLock() }.getOrNull() == true) {
|
||||||
|
try {
|
||||||
|
rawModeRequested = false
|
||||||
|
val s = rawSavedAttributes
|
||||||
|
rawSavedAttributes = null
|
||||||
|
s
|
||||||
|
} finally {
|
||||||
|
stateMutex.unlock()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (saved != null) {
|
||||||
|
runCatching { term.setAttributes(saved) }
|
||||||
|
.onFailure { consoleFlowDebug("jline-events: shutdown raw attrs restore failed", it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun currentTerminal(): Terminal? {
|
private fun currentTerminal(): Terminal? {
|
||||||
val existing = terminalRef.get()
|
val existing = terminalRef.get()
|
||||||
@ -270,13 +309,13 @@ object JvmLyngConsole : LyngConsole {
|
|||||||
.onFailure { consoleFlowDebug("jline-events: reader shutdown failed during recovery", it) }
|
.onFailure { consoleFlowDebug("jline-events: reader shutdown failed during recovery", it) }
|
||||||
|
|
||||||
reader = activeTerm.reader()
|
reader = activeTerm.reader()
|
||||||
if (reader.hashCode() == prevReader.hashCode()) {
|
if (reader === prevReader) {
|
||||||
consoleFlowDebug("jline-events: reader recovery no-op oldReader=${prevReader.hashCode()} newReader=${reader.hashCode()} -> forcing terminal rebuild")
|
consoleFlowDebug("jline-events: reader recovery no-op oldReader=${System.identityHashCode(prevReader)} newReader=${System.identityHashCode(reader)} -> forcing terminal rebuild")
|
||||||
if (!tryRebuildTerminal()) {
|
if (!tryRebuildTerminal()) {
|
||||||
consoleFlowDebug("jline-events: forced terminal rebuild did not produce a new reader")
|
consoleFlowDebug("jline-events: forced terminal rebuild did not produce a new reader")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
consoleFlowDebug("jline-events: reader recovered oldReader=${prevReader.hashCode()} newReader=${reader.hashCode()}")
|
consoleFlowDebug("jline-events: reader recovered oldReader=${System.identityHashCode(prevReader)} newReader=${System.identityHashCode(reader)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
readerRecoveries.incrementAndGet()
|
readerRecoveries.incrementAndGet()
|
||||||
@ -305,18 +344,56 @@ object JvmLyngConsole : LyngConsole {
|
|||||||
} else {
|
} else {
|
||||||
keySendFailures.incrementAndGet()
|
keySendFailures.incrementAndGet()
|
||||||
}
|
}
|
||||||
} catch (_: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
break
|
// Keep input alive if this is a transient interrupt while still running.
|
||||||
|
if (!running.get() || !keyLoopRunning.get()) break
|
||||||
|
recoveryRequested.set(true)
|
||||||
|
consoleFlowDebug("jline-events: key-reader interrupted; scheduling reader recovery", e)
|
||||||
|
Thread.interrupted()
|
||||||
|
continue
|
||||||
|
} catch (e: InterruptedIOException) {
|
||||||
|
// Common during reader shutdown/rebind. Recover silently and keep input flowing.
|
||||||
|
if (!running.get() || !keyLoopRunning.get()) break
|
||||||
|
recoveryRequested.set(true)
|
||||||
|
consoleFlowDebug("jline-events: read interrupted; scheduling reader recovery", e)
|
||||||
|
try {
|
||||||
|
Thread.sleep(10)
|
||||||
|
} catch (ie: InterruptedException) {
|
||||||
|
if (!running.get() || !keyLoopRunning.get()) break
|
||||||
|
recoveryRequested.set(true)
|
||||||
|
consoleFlowDebug("jline-events: interrupted during recovery backoff; continuing", ie)
|
||||||
|
Thread.interrupted()
|
||||||
|
}
|
||||||
|
} catch (e: EOFException) {
|
||||||
|
// EOF from reader should trigger rebind/rebuild rather than ending input stream.
|
||||||
|
if (!running.get() || !keyLoopRunning.get()) break
|
||||||
|
recoveryRequested.set(true)
|
||||||
|
consoleFlowDebug("jline-events: reader EOF; scheduling reader recovery", e)
|
||||||
|
try {
|
||||||
|
Thread.sleep(20)
|
||||||
|
} catch (ie: InterruptedException) {
|
||||||
|
if (!running.get() || !keyLoopRunning.get()) break
|
||||||
|
recoveryRequested.set(true)
|
||||||
|
consoleFlowDebug("jline-events: interrupted during EOF backoff; continuing", ie)
|
||||||
|
Thread.interrupted()
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
readFailures.incrementAndGet()
|
readFailures.incrementAndGet()
|
||||||
|
recoveryRequested.set(true)
|
||||||
consoleFlowDebug("jline-events: blocking read failed", e)
|
consoleFlowDebug("jline-events: blocking read failed", e)
|
||||||
try {
|
try {
|
||||||
Thread.sleep(50)
|
Thread.sleep(50)
|
||||||
} catch (_: InterruptedException) {
|
} catch (ie: InterruptedException) {
|
||||||
break
|
if (!running.get() || !keyLoopRunning.get()) break
|
||||||
|
recoveryRequested.set(true)
|
||||||
|
consoleFlowDebug("jline-events: interrupted during error backoff; continuing", ie)
|
||||||
|
Thread.interrupted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
consoleFlowDebug(
|
||||||
|
"jline-events: key-reader thread stopped running=${running.get()} keyLoopRunning=${keyLoopRunning.get()} loops=${keyLoopCount.get()} keys=${keyEvents.get()} readFailures=${readFailures.get()}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
heartbeatThread = thread(start = true, isDaemon = true, name = "lyng-jline-heartbeat") {
|
heartbeatThread = thread(start = true, isDaemon = true, name = "lyng-jline-heartbeat") {
|
||||||
@ -334,13 +411,19 @@ object JvmLyngConsole : LyngConsole {
|
|||||||
val readBlockedMs = if (readStartNs > 0L && readEndNs < readStartNs) {
|
val readBlockedMs = if (readStartNs > 0L && readEndNs < readStartNs) {
|
||||||
(System.nanoTime() - readStartNs) / 1_000_000L
|
(System.nanoTime() - readStartNs) / 1_000_000L
|
||||||
} else 0L
|
} else 0L
|
||||||
if (requested && keyCodesRead.get() > 0L && idleMs >= 1400L) {
|
val streamIdle = requested && keyCodesRead.get() > 0L && idleMs >= 1400L
|
||||||
|
val readStalled = requested && readBlockedMs >= 1600L
|
||||||
|
if (streamIdle || readStalled) {
|
||||||
val sinceRecoveryMs = (System.nanoTime() - lastRecoveryNs.get()) / 1_000_000L
|
val sinceRecoveryMs = (System.nanoTime() - lastRecoveryNs.get()) / 1_000_000L
|
||||||
if (sinceRecoveryMs >= 1200L) {
|
if (sinceRecoveryMs >= 1200L) {
|
||||||
recoveryRequested.set(true)
|
recoveryRequested.set(true)
|
||||||
|
if (readStalled) {
|
||||||
|
consoleFlowDebug("jline-events: key read blocked ${readBlockedMs}ms; scheduling reader recovery")
|
||||||
|
} else {
|
||||||
consoleFlowDebug("jline-events: key stream idle ${idleMs}ms; scheduling reader recovery")
|
consoleFlowDebug("jline-events: key stream idle ${idleMs}ms; scheduling reader recovery")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
consoleFlowDebug(
|
consoleFlowDebug(
|
||||||
"jline-events: heartbeat keyCodes=${keyCodesRead.get()} keysSent=${keyEvents.get()} sendFailures=${keySendFailures.get()} readFailures=${readFailures.get()} recoveries=${readerRecoveries.get()} rawRequested=$requested keyLoop=${keyLoopCount.get()} readBlockedMs=$readBlockedMs keyIdleMs=$idleMs keyPath=reader"
|
"jline-events: heartbeat keyCodes=${keyCodesRead.get()} keysSent=${keyEvents.get()} sendFailures=${keySendFailures.get()} readFailures=${readFailures.get()} recoveries=${readerRecoveries.get()} rawRequested=$requested keyLoop=${keyLoopCount.get()} readBlockedMs=$readBlockedMs keyIdleMs=$idleMs keyPath=reader"
|
||||||
)
|
)
|
||||||
@ -366,6 +449,7 @@ object JvmLyngConsole : LyngConsole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun close() {
|
override suspend fun close() {
|
||||||
|
consoleFlowDebug("jline-events: collector close requested", Throwable("collector close caller"))
|
||||||
cleanup()
|
cleanup()
|
||||||
consoleFlowDebug(
|
consoleFlowDebug(
|
||||||
"jline-events: collector ended keys=${keyEvents.get()} readFailures=${readFailures.get()}"
|
"jline-events: collector ended keys=${keyEvents.get()} readFailures=${readFailures.get()}"
|
||||||
@ -485,7 +569,9 @@ object JvmLyngConsole : LyngConsole {
|
|||||||
ctrl: Boolean = false,
|
ctrl: Boolean = false,
|
||||||
alt: Boolean = false,
|
alt: Boolean = false,
|
||||||
shift: Boolean = false,
|
shift: Boolean = false,
|
||||||
): ConsoleEvent.KeyDown = ConsoleEvent.KeyDown(
|
): ConsoleEvent.KeyDown {
|
||||||
|
require(value.isNotEmpty()) { "ConsoleEvent.KeyDown.key must never be empty" }
|
||||||
|
return ConsoleEvent.KeyDown(
|
||||||
key = value,
|
key = value,
|
||||||
code = null,
|
code = null,
|
||||||
ctrl = ctrl,
|
ctrl = ctrl,
|
||||||
@ -494,3 +580,4 @@ object JvmLyngConsole : LyngConsole {
|
|||||||
meta = false
|
meta = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class LyngConsoleModuleTest {
|
|||||||
val d = Console.details()
|
val d = Console.details()
|
||||||
assert(d.supported is Bool)
|
assert(d.supported is Bool)
|
||||||
assert(d.isTty is Bool)
|
assert(d.isTty is Bool)
|
||||||
assert(d.ansiLevel is String)
|
assert(d.ansiLevel is ConsoleAnsiLevel)
|
||||||
|
|
||||||
val g = Console.geometry()
|
val g = Console.geometry()
|
||||||
if (g != null) {
|
if (g != null) {
|
||||||
|
|||||||
157
lyngio/stdlib/lyng/io/console.lyng
Normal file
157
lyngio/stdlib/lyng/io/console.lyng
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package lyng.io.console
|
||||||
|
|
||||||
|
/* Console event kinds used by `ConsoleEvent.type`. */
|
||||||
|
enum ConsoleEventType {
|
||||||
|
UNKNOWN,
|
||||||
|
RESIZE,
|
||||||
|
KEY_DOWN,
|
||||||
|
KEY_UP
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normalized key codes used by `ConsoleKeyEvent.code`. */
|
||||||
|
enum ConsoleKeyCode {
|
||||||
|
UNKNOWN,
|
||||||
|
CHARACTER,
|
||||||
|
ARROW_UP,
|
||||||
|
ARROW_DOWN,
|
||||||
|
ARROW_LEFT,
|
||||||
|
ARROW_RIGHT,
|
||||||
|
HOME,
|
||||||
|
END,
|
||||||
|
INSERT,
|
||||||
|
DELETE,
|
||||||
|
PAGE_UP,
|
||||||
|
PAGE_DOWN,
|
||||||
|
ESCAPE,
|
||||||
|
ENTER,
|
||||||
|
TAB,
|
||||||
|
BACKSPACE,
|
||||||
|
SPACE
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detected ANSI terminal capability level. */
|
||||||
|
enum ConsoleAnsiLevel {
|
||||||
|
NONE,
|
||||||
|
BASIC16,
|
||||||
|
ANSI256,
|
||||||
|
TRUECOLOR
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base class for console events. */
|
||||||
|
extern class ConsoleEvent {
|
||||||
|
/* Event kind for stable matching/switching. */
|
||||||
|
val type: ConsoleEventType
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal resize event. */
|
||||||
|
extern class ConsoleResizeEvent : ConsoleEvent {
|
||||||
|
/* Current terminal width in character cells. */
|
||||||
|
val columns: Int
|
||||||
|
/* Current terminal height in character cells. */
|
||||||
|
val rows: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyboard event. */
|
||||||
|
extern class ConsoleKeyEvent : ConsoleEvent {
|
||||||
|
/*
|
||||||
|
Logical key name normalized for app-level handling, for example:
|
||||||
|
"a", "A", "ArrowLeft", "Escape", "Enter".
|
||||||
|
*/
|
||||||
|
val key: String
|
||||||
|
/* Normalized key code enum for robust matching independent of backend specifics. */
|
||||||
|
val code: ConsoleKeyCode
|
||||||
|
/*
|
||||||
|
Optional backend-specific raw identifier (if available).
|
||||||
|
Not guaranteed to be present or stable across platforms.
|
||||||
|
*/
|
||||||
|
val codeName: String?
|
||||||
|
/* True when Ctrl was pressed during the key event. */
|
||||||
|
val ctrl: Bool
|
||||||
|
/* True when Alt/Option was pressed during the key event. */
|
||||||
|
val alt: Bool
|
||||||
|
/* True when Shift was pressed during the key event. */
|
||||||
|
val shift: Bool
|
||||||
|
/* True when Meta/Super/Command was pressed during the key event. */
|
||||||
|
val meta: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pull iterator over console events. */
|
||||||
|
extern class ConsoleEventIterator : Iterator<ConsoleEvent> {
|
||||||
|
/* Whether another event is currently available from the stream. */
|
||||||
|
override fun hasNext(): Bool
|
||||||
|
/* Returns next event or throws iteration-finished when exhausted/cancelled. */
|
||||||
|
override fun next(): ConsoleEvent
|
||||||
|
/* Stops this iterator. The underlying console service remains managed by runtime. */
|
||||||
|
override fun cancelIteration(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Endless iterable console event stream. */
|
||||||
|
extern class ConsoleEventStream : Iterable<ConsoleEvent> {
|
||||||
|
/* Creates a fresh event iterator bound to the current console input stream. */
|
||||||
|
override fun iterator(): ConsoleEventIterator
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal geometry in character cells. */
|
||||||
|
extern class ConsoleGeometry {
|
||||||
|
val columns: Int
|
||||||
|
val rows: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Snapshot of console support/capabilities. */
|
||||||
|
extern class ConsoleDetails {
|
||||||
|
/* True when current runtime has console control implementation. */
|
||||||
|
val supported: Bool
|
||||||
|
/* True when output/input are attached to an interactive terminal. */
|
||||||
|
val isTty: Bool
|
||||||
|
/* Detected terminal color capability level. */
|
||||||
|
val ansiLevel: ConsoleAnsiLevel
|
||||||
|
/* Current terminal size if available, otherwise null. */
|
||||||
|
val geometry: ConsoleGeometry?
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Console API singleton object. */
|
||||||
|
extern object Console {
|
||||||
|
/* Returns true when console control API is implemented in this runtime. */
|
||||||
|
fun isSupported(): Bool
|
||||||
|
/* Returns true when process is attached to interactive TTY. */
|
||||||
|
fun isTty(): Bool
|
||||||
|
/* Returns detected color capability level. */
|
||||||
|
fun ansiLevel(): ConsoleAnsiLevel
|
||||||
|
/* Returns current terminal geometry, or null when unavailable. */
|
||||||
|
fun geometry(): ConsoleGeometry?
|
||||||
|
/* Returns combined capability snapshot in one call. */
|
||||||
|
fun details(): ConsoleDetails
|
||||||
|
|
||||||
|
/* Writes raw text to console output buffer (no implicit newline). */
|
||||||
|
fun write(text: String): void
|
||||||
|
/* Flushes pending console output. Call after batched writes. */
|
||||||
|
fun flush(): void
|
||||||
|
|
||||||
|
/* Moves cursor to home position (row 1, column 1). */
|
||||||
|
fun home(): void
|
||||||
|
/* Clears visible screen buffer. Cursor position is backend-dependent after clear. */
|
||||||
|
fun clear(): void
|
||||||
|
/* Moves cursor to 1-based row/column. Values outside viewport are backend-defined. */
|
||||||
|
fun moveTo(row: Int, column: Int): void
|
||||||
|
/* Clears current line content. Cursor stays on the same line. */
|
||||||
|
fun clearLine(): void
|
||||||
|
|
||||||
|
/* Switches terminal into alternate screen buffer (useful for TUIs). */
|
||||||
|
fun enterAltScreen(): void
|
||||||
|
/* Returns from alternate screen buffer to the normal terminal screen. */
|
||||||
|
fun leaveAltScreen(): void
|
||||||
|
/* Shows or hides the cursor. Prefer restoring visibility in finally blocks. */
|
||||||
|
fun setCursorVisible(visible: Bool): void
|
||||||
|
|
||||||
|
/*
|
||||||
|
Returns endless event stream (resize + key events).
|
||||||
|
Typical usage is consuming in a launched loop.
|
||||||
|
*/
|
||||||
|
fun events(): ConsoleEventStream
|
||||||
|
|
||||||
|
/*
|
||||||
|
Enables/disables raw keyboard mode.
|
||||||
|
Returns true when state was actually changed.
|
||||||
|
*/
|
||||||
|
fun setRawMode(enabled: Bool): Bool
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "1.5.0-RC"
|
version = "1.5.0-RC2"
|
||||||
|
|
||||||
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,7 @@ extern fun abs(x: Object): Real
|
|||||||
extern fun ln(x: Object): Real
|
extern fun ln(x: Object): Real
|
||||||
extern fun pow(x: Object, y: Object): Real
|
extern fun pow(x: Object, y: Object): Real
|
||||||
extern fun sqrt(x: Object): Real
|
extern fun sqrt(x: Object): Real
|
||||||
|
extern fun clamp<T>(value: T, range: Range<T>): T
|
||||||
|
|
||||||
class SeededRandom {
|
class SeededRandom {
|
||||||
extern fun nextInt(): Int
|
extern fun nextInt(): Int
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user