From 7b8c202043ec1cdeccd12c5bdf10312a4499438d Mon Sep 17 00:00:00 2001 From: EpicDima Date: Mon, 19 Aug 2024 18:34:45 +0300 Subject: [PATCH 1/3] Remove last empty line in `AnsiRendering` (used for console output) --- .../kotlin/com/jakewharton/mosaic/ansi.kt | 14 +++- .../com/jakewharton/mosaic/rendering.kt | 72 +++++++++---------- .../jakewharton/mosaic/AnsiRenderingTest.kt | 35 ++++----- .../com/jakewharton/mosaic/CounterTest.kt | 13 +--- .../com/jakewharton/mosaic/MosaicTest.kt | 18 ++--- .../mosaic/TestMosaicComposition.kt | 6 +- .../kotlin/com/jakewharton/mosaic/stuff.kt | 4 ++ .../rrtop/src/main/kotlin/example/RrtopApp.kt | 5 +- 8 files changed, 73 insertions(+), 94 deletions(-) diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ansi.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ansi.kt index cc41f6314..ae75c1280 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ansi.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ansi.kt @@ -1,3 +1,5 @@ +@file:Suppress("NOTHING_TO_INLINE") + package com.jakewharton.mosaic import com.github.ajalt.mordant.rendering.AnsiLevel as MordantAnsiLevel @@ -5,15 +7,15 @@ import com.jakewharton.mosaic.ui.AnsiLevel import com.jakewharton.mosaic.ui.Color import kotlin.math.roundToInt -private const val ESC = "\u001B" +internal const val ESC = "\u001B" internal const val CSI = "$ESC[" internal const val ansiBeginSynchronizedUpdate = "$CSI?2026h" internal const val ansiEndSynchronizedUpdate = "$CSI?2026l" internal const val ansiReset = "${CSI}0" -internal const val clearLine = "${CSI}K" -internal const val cursorUp = "${CSI}F" +internal const val ansiMoveCursorToFirstColumn = "${CSI}0G" +internal const val ansiClearAllAfterCursor = "${CSI}0J" internal const val cursorHide = "$CSI?25l" internal const val cursorShow = "$CSI?25h" @@ -32,6 +34,12 @@ internal const val ansiBgColorOffset = 10 internal const val ansiSelectorColor256 = 5 internal const val ansiSelectorColorRgb = 2 +internal inline fun StringBuilder.ansiCursorUp(lines: Int) { + append(CSI) + append(lines) + append("A") +} + internal fun MordantAnsiLevel.toMosaicAnsiLevel(): AnsiLevel { return when (this) { MordantAnsiLevel.NONE -> AnsiLevel.NONE diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt index f3cbe8e26..7735f2833 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt @@ -71,7 +71,7 @@ internal class DebugRendering( internal class AnsiRendering( private val ansiLevel: AnsiLevel = AnsiLevel.TRUECOLOR, ) : Rendering { - private val stringBuilder = StringBuilder(100) + private val stringBuilder = StringBuilder(128) private val staticSurfaces = mutableObjectListOf() private var lastHeight = 0 @@ -81,51 +81,45 @@ internal class AnsiRendering( append(ansiBeginSynchronizedUpdate) - var staleLines = lastHeight - repeat(staleLines) { - append(cursorUp) - } - - fun appendSurface(canvas: TextSurface) { - for (row in 0 until canvas.height) { - canvas.appendRowTo(this, row) - if (staleLines-- > 0) { - // We have previously drawn on this line. Clear the rest to be safe. - append(clearLine) - } - append("\r\n") - } - } - - staticSurfaces.let { staticSurfaces -> - node.paintStatics(staticSurfaces, ansiLevel) - if (staticSurfaces.isNotEmpty()) { - staticSurfaces.forEach { staticSurface -> - appendSurface(staticSurface) - } - staticSurfaces.clear() - } + // don't need to move cursor up if there was zero or one line + if (lastHeight > 1) { + ansiCursorUp(lastHeight - 1) } + append(ansiMoveCursorToFirstColumn) + append(ansiClearAllAfterCursor) + + node.measureAndPlace() + + var afterStatic = false // in order not to overwrite last line of static output + staticSurfaces.let { staticSurfaces -> + node.paintStatics(staticSurfaces, ansiLevel) + if (staticSurfaces.isNotEmpty()) { + staticSurfaces.forEach { staticSurface -> + appendSurface(staticSurface, addLineBreakAtBeginning = false) + if (!afterStatic && staticSurface.height > 0) { + afterStatic = true + } + } + staticSurfaces.clear() + } + } val surface = node.paint(ansiLevel) - appendSurface(surface) - - // If the new output contains fewer lines than the last output, clear those old lines. - for (i in 0 until staleLines) { - if (i > 0) { - append("\r\n") - } - append(clearLine) - } - - // Move cursor back up to end of the new output. - repeat(staleLines - 1) { - append(cursorUp) + if (node.height > 0) { + appendSurface(surface, addLineBreakAtBeginning = afterStatic) } + lastHeight = surface.height append(ansiEndSynchronizedUpdate) + } + } - lastHeight = surface.height + private fun StringBuilder.appendSurface(canvas: TextSurface, addLineBreakAtBeginning: Boolean) { + for (row in 0 until canvas.height) { + if (row > 0 || (row == 0 && addLineBreakAtBeginning)) { + append("\r\n") + } + canvas.appendRowTo(this, row) } } } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt index 6b3eb8358..751cb85bf 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt @@ -23,9 +23,8 @@ class AnsiRenderingTest { // TODO We should not draw trailing whitespace. assertThat(rendering.render(rootNode).toString()).isEqualTo( """ - |Hello$s + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hello$s |World! - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -40,9 +39,8 @@ class AnsiRenderingTest { assertThat(rendering.render(firstRootNode).toString()).isEqualTo( """ - |Hello$s + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hello$s |World! - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) @@ -57,11 +55,10 @@ class AnsiRenderingTest { assertThat(rendering.render(secondRootNode).toString()).isEqualTo( """ - |$cursorUp${cursorUp}Hel$clearLine - |lo $clearLine + |${ansiCursorUp(1)}${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hel + |lo$s |Wor |ld! - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -78,11 +75,10 @@ class AnsiRenderingTest { assertThat(rendering.render(firstRootNode).toString()).isEqualTo( """ - |Hel + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hel |lo$s |Wor |ld! - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) @@ -95,10 +91,8 @@ class AnsiRenderingTest { assertThat(rendering.render(secondRootNode).toString()).isEqualTo( """ - |$cursorUp$cursorUp$cursorUp${cursorUp}Hello $clearLine - |World!$clearLine - |$clearLine - |$clearLine$cursorUp + |${ansiCursorUp(3)}${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hello$s + |World! """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -113,9 +107,8 @@ class AnsiRenderingTest { assertThat(rendering.render(rootNode).toString()).isEqualTo( """ - |World! + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}World! |Hello - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -130,9 +123,8 @@ class AnsiRenderingTest { assertThat(rendering.render(firstRootNode).toString()).isEqualTo( """ - |One + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One |Two - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) @@ -145,9 +137,8 @@ class AnsiRenderingTest { assertThat(rendering.render(secondRootNode).toString()).isEqualTo( """ - |${cursorUp}Three$clearLine + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Three |Four - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -178,13 +169,12 @@ class AnsiRenderingTest { assertThat(rendering.render(rootNode).toString()).isEqualTo( """ - |One + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s |Two |Three |Four |Five |Sup - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -204,10 +194,9 @@ class AnsiRenderingTest { assertThat(rendering.render(firstRootNode).toString()).isEqualTo( """ - |Static + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Static |TopTopTop |LeftLeft$s - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/CounterTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/CounterTest.kt index 46ab40058..a8624e347 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/CounterTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/CounterTest.kt @@ -33,18 +33,9 @@ class CounterTest { @Test fun counterWithAnsi() = runTest { runMosaicTest(withAnsi = true) { setCounter() - assertThat(awaitRenderSnapshot()).isEqualTo( - """ - |${ansiBeginSynchronizedUpdate}The count is: 0 - |$ansiEndSynchronizedUpdate - """.trimMargin().replaceLineEndingsWithCRLF(), - ) - for (i in 1..20) { + for (i in 0..20) { assertThat(awaitRenderSnapshot()).isEqualTo( - """ - |${ansiBeginSynchronizedUpdate}${cursorUp}The count is: ${i}$clearLine - |$ansiEndSynchronizedUpdate - """.trimMargin().replaceLineEndingsWithCRLF(), + "${ansiBeginSynchronizedUpdate}${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}The count is: ${i}$ansiEndSynchronizedUpdate", ) } } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt index 3d397e48b..169b87da3 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt @@ -45,10 +45,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |One $s + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s |Two $s |Three - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -75,10 +74,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |One $s + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s |Two $s |Three - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -105,10 +103,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |One $s + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s |Two $s |Three - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -147,10 +144,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |One $s + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s |Two $s |Three - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -167,10 +163,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |One $s + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s |Two $s |Three - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -191,10 +186,9 @@ class MosaicTest { actuals.forEach { actual -> assertThat(actual).isEqualTo( """ - |One $s + |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s |Two $s |Three - | """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt index 0115ef84d..e43e476ac 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt @@ -80,9 +80,9 @@ private class RealTestMosaicComposition( } else { rendering.render(rootNode).toString() .removeSurrounding(ansiBeginSynchronizedUpdate, ansiEndSynchronizedUpdate) - .removeSuffix("\r\n") // without last line break for simplicity - .replace(clearLine, "") - .replace(cursorUp, "") + .replace(Regex("$ESC\\[\\d+A"), "") + .removePrefix(ansiMoveCursorToFirstColumn) + .removePrefix(ansiClearAllAfterCursor) .replace("\r\n", "\n") // CRLF to LF for simplicity } renderSnapshots.trySend(stringRender) diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt index 01d5f02b2..d057decb1 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt @@ -27,6 +27,10 @@ const val s = " " const val TestChar = 'X' +fun ansiCursorUp(lines: Int): String { + return buildString { ansiCursorUp(lines) } +} + fun String.replaceLineEndingsWithCRLF(): String { return this.replace("\n", "\r\n") } diff --git a/samples/rrtop/src/main/kotlin/example/RrtopApp.kt b/samples/rrtop/src/main/kotlin/example/RrtopApp.kt index 0d32eb2ac..f8851a2e4 100644 --- a/samples/rrtop/src/main/kotlin/example/RrtopApp.kt +++ b/samples/rrtop/src/main/kotlin/example/RrtopApp.kt @@ -11,7 +11,7 @@ import com.jakewharton.mosaic.layout.fillMaxWidth import com.jakewharton.mosaic.layout.height import com.jakewharton.mosaic.layout.onKeyEvent import com.jakewharton.mosaic.layout.padding -import com.jakewharton.mosaic.layout.width +import com.jakewharton.mosaic.layout.size import com.jakewharton.mosaic.modifier.Modifier import com.jakewharton.mosaic.text.SpanStyle import com.jakewharton.mosaic.text.buildAnnotatedString @@ -32,8 +32,7 @@ fun RrtopApp(rrtopViewModel: RrtopViewModel, colorsPalette: RrtopColorsPalette) CompositionLocalProvider(LocalRrtopColorsPalette provides colorsPalette) { Box( modifier = Modifier - .width(terminal.size.width) - .height(terminal.size.height - 1) // subtraction of one is necessary, because there is a line with a cursor at the bottom, which moves up all the content + .size(terminal.size) .background(LocalRrtopColorsPalette.current.mainBg) .onKeyEvent { when (it) { From f9fe2ba1138a298036257fdc410e4f4e9625926b Mon Sep 17 00:00:00 2001 From: EpicDima Date: Tue, 20 Aug 2024 18:07:01 +0300 Subject: [PATCH 2/3] Use ANSI clear line on every row If you run the program in the terminal without synchronization, a complete cleanup at the beginning will subsequently lead to blinks, which will be more frequent than usual, so we use cleaning individual lines so that the overwriting goes from top to bottom line by line. This makes the number of commands larger, but it may not be worth it, so it makes sense to think about completely cleaning the terminal (not including static output, of course) without clearing individual lines. --- .../kotlin/com/jakewharton/mosaic/ansi.kt | 6 +- .../com/jakewharton/mosaic/rendering.kt | 11 ++-- .../jakewharton/mosaic/AnsiRenderingTest.kt | 58 +++++++++---------- .../com/jakewharton/mosaic/CounterTest.kt | 2 +- .../com/jakewharton/mosaic/MosaicTest.kt | 36 ++++++------ .../mosaic/TestMosaicComposition.kt | 1 + .../kotlin/com/jakewharton/mosaic/stuff.kt | 4 +- 7 files changed, 61 insertions(+), 57 deletions(-) diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ansi.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ansi.kt index ae75c1280..c84718f71 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ansi.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/ansi.kt @@ -13,10 +13,12 @@ internal const val CSI = "$ESC[" internal const val ansiBeginSynchronizedUpdate = "$CSI?2026h" internal const val ansiEndSynchronizedUpdate = "$CSI?2026l" -internal const val ansiReset = "${CSI}0" +internal const val ansiClearLineAfterCursor = "${CSI}K" internal const val ansiMoveCursorToFirstColumn = "${CSI}0G" internal const val ansiClearAllAfterCursor = "${CSI}0J" +internal const val ansiReset = "${CSI}0" + internal const val cursorHide = "$CSI?25l" internal const val cursorShow = "$CSI?25h" @@ -34,7 +36,7 @@ internal const val ansiBgColorOffset = 10 internal const val ansiSelectorColor256 = 5 internal const val ansiSelectorColorRgb = 2 -internal inline fun StringBuilder.ansiCursorUp(lines: Int) { +internal inline fun StringBuilder.ansiMoveCursorUp(lines: Int) { append(CSI) append(lines) append("A") diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt index 7735f2833..49b786edd 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt @@ -83,10 +83,9 @@ internal class AnsiRendering( // don't need to move cursor up if there was zero or one line if (lastHeight > 1) { - ansiCursorUp(lastHeight - 1) + ansiMoveCursorUp(lastHeight - 1) } append(ansiMoveCursorToFirstColumn) - append(ansiClearAllAfterCursor) node.measureAndPlace() @@ -110,16 +109,18 @@ internal class AnsiRendering( } lastHeight = surface.height + append(ansiClearAllAfterCursor) append(ansiEndSynchronizedUpdate) } } private fun StringBuilder.appendSurface(canvas: TextSurface, addLineBreakAtBeginning: Boolean) { - for (row in 0 until canvas.height) { - if (row > 0 || (row == 0 && addLineBreakAtBeginning)) { + for (rowIndex in 0 until canvas.height) { + if (rowIndex > 0 || (rowIndex == 0 && addLineBreakAtBeginning)) { append("\r\n") } - canvas.appendRowTo(this, row) + canvas.appendRowTo(this, rowIndex) + append(ansiClearLineAfterCursor) } } } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt index 751cb85bf..e4608fd68 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt @@ -23,8 +23,8 @@ class AnsiRenderingTest { // TODO We should not draw trailing whitespace. assertThat(rendering.render(rootNode).toString()).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hello$s - |World! + |${ansiMoveCursorToFirstColumn}Hello $ansiClearLineAfterCursor + |World!$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -39,8 +39,8 @@ class AnsiRenderingTest { assertThat(rendering.render(firstRootNode).toString()).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hello$s - |World! + |${ansiMoveCursorToFirstColumn}Hello $ansiClearLineAfterCursor + |World!$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) @@ -55,10 +55,10 @@ class AnsiRenderingTest { assertThat(rendering.render(secondRootNode).toString()).isEqualTo( """ - |${ansiCursorUp(1)}${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hel - |lo$s - |Wor - |ld! + |${ansiMoveCursorUp(1)}${ansiMoveCursorToFirstColumn}Hel$ansiClearLineAfterCursor + |lo $ansiClearLineAfterCursor + |Wor$ansiClearLineAfterCursor + |ld!$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -75,10 +75,10 @@ class AnsiRenderingTest { assertThat(rendering.render(firstRootNode).toString()).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hel - |lo$s - |Wor - |ld! + |${ansiMoveCursorToFirstColumn}Hel$ansiClearLineAfterCursor + |lo $ansiClearLineAfterCursor + |Wor$ansiClearLineAfterCursor + |ld!$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) @@ -91,8 +91,8 @@ class AnsiRenderingTest { assertThat(rendering.render(secondRootNode).toString()).isEqualTo( """ - |${ansiCursorUp(3)}${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Hello$s - |World! + |${ansiMoveCursorUp(3)}${ansiMoveCursorToFirstColumn}Hello $ansiClearLineAfterCursor + |World!$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -107,8 +107,8 @@ class AnsiRenderingTest { assertThat(rendering.render(rootNode).toString()).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}World! - |Hello + |${ansiMoveCursorToFirstColumn}World!$ansiClearLineAfterCursor + |Hello$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -123,8 +123,8 @@ class AnsiRenderingTest { assertThat(rendering.render(firstRootNode).toString()).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One - |Two + |${ansiMoveCursorToFirstColumn}One$ansiClearLineAfterCursor + |Two$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) @@ -137,8 +137,8 @@ class AnsiRenderingTest { assertThat(rendering.render(secondRootNode).toString()).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Three - |Four + |${ansiMoveCursorToFirstColumn}Three$ansiClearLineAfterCursor + |Four$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -169,12 +169,12 @@ class AnsiRenderingTest { assertThat(rendering.render(rootNode).toString()).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s - |Two - |Three - |Four - |Five - |Sup + |${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor + |Two $ansiClearLineAfterCursor + |Three$ansiClearLineAfterCursor + |Four $ansiClearLineAfterCursor + |Five $ansiClearLineAfterCursor + |Sup$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -194,9 +194,9 @@ class AnsiRenderingTest { assertThat(rendering.render(firstRootNode).toString()).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}Static - |TopTopTop - |LeftLeft$s + |${ansiMoveCursorToFirstColumn}Static$ansiClearLineAfterCursor + |TopTopTop$ansiClearLineAfterCursor + |LeftLeft $ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/CounterTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/CounterTest.kt index a8624e347..e9538ea3a 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/CounterTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/CounterTest.kt @@ -35,7 +35,7 @@ class CounterTest { setCounter() for (i in 0..20) { assertThat(awaitRenderSnapshot()).isEqualTo( - "${ansiBeginSynchronizedUpdate}${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}The count is: ${i}$ansiEndSynchronizedUpdate", + "${ansiMoveCursorToFirstColumn}The count is: ${i}$ansiClearLineAfterCursor$ansiClearAllAfterCursor".wrapWithAnsiSynchronizedUpdate(), ) } } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt index 169b87da3..fca6cc074 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/MosaicTest.kt @@ -45,9 +45,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s - |Two $s - |Three + |${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor + |Two $ansiClearLineAfterCursor + |Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -74,9 +74,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s - |Two $s - |Three + |${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor + |Two $ansiClearLineAfterCursor + |Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -103,9 +103,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s - |Two $s - |Three + |${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor + |Two $ansiClearLineAfterCursor + |Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -144,9 +144,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s - |Two $s - |Three + |${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor + |Two $ansiClearLineAfterCursor + |Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -163,9 +163,9 @@ class MosaicTest { } assertThat(actual).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s - |Two $s - |Three + |${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor + |Two $ansiClearLineAfterCursor + |Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } @@ -186,9 +186,9 @@ class MosaicTest { actuals.forEach { actual -> assertThat(actual).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}${ansiClearAllAfterCursor}One $s - |Two $s - |Three + |${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor + |Two $ansiClearLineAfterCursor + |Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) } diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt index e43e476ac..56691bdf9 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt @@ -83,6 +83,7 @@ private class RealTestMosaicComposition( .replace(Regex("$ESC\\[\\d+A"), "") .removePrefix(ansiMoveCursorToFirstColumn) .removePrefix(ansiClearAllAfterCursor) + .replace(ansiClearLineAfterCursor, "") .replace("\r\n", "\n") // CRLF to LF for simplicity } renderSnapshots.trySend(stringRender) diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt index d057decb1..defb1ac3b 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/stuff.kt @@ -27,8 +27,8 @@ const val s = " " const val TestChar = 'X' -fun ansiCursorUp(lines: Int): String { - return buildString { ansiCursorUp(lines) } +fun ansiMoveCursorUp(lines: Int): String { + return buildString { ansiMoveCursorUp(lines) } } fun String.replaceLineEndingsWithCRLF(): String { From 3504372782ddbb998d5d7c415fda3f98d4f2eede Mon Sep 17 00:00:00 2001 From: EpicDima Date: Sun, 27 Oct 2024 18:14:59 +0300 Subject: [PATCH 3/3] Fix after rebase --- CHANGELOG.md | 2 +- .../com/jakewharton/mosaic/rendering.kt | 32 +++++++++---------- .../jakewharton/mosaic/AnsiRenderingTest.kt | 8 ++--- .../mosaic/TestMosaicComposition.kt | 8 ++--- .../rrtop/src/main/kotlin/example/RrtopApp.kt | 2 +- 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 062270eed..6fc260389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ New: - Nothing yet! Changed: -- Nothing yet! +- An empty line is not added at the end of the output. Fixed: - Nothing yet! diff --git a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt index 49b786edd..8b9369403 100644 --- a/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt +++ b/mosaic-runtime/src/commonMain/kotlin/com/jakewharton/mosaic/rendering.kt @@ -83,29 +83,27 @@ internal class AnsiRendering( // don't need to move cursor up if there was zero or one line if (lastHeight > 1) { - ansiMoveCursorUp(lastHeight - 1) + ansiMoveCursorUp(lastHeight - 1) } append(ansiMoveCursorToFirstColumn) - node.measureAndPlace() - - var afterStatic = false // in order not to overwrite last line of static output - staticSurfaces.let { staticSurfaces -> - node.paintStatics(staticSurfaces, ansiLevel) - if (staticSurfaces.isNotEmpty()) { - staticSurfaces.forEach { staticSurface -> - appendSurface(staticSurface, addLineBreakAtBeginning = false) - if (!afterStatic && staticSurface.height > 0) { - afterStatic = true - } - } - staticSurfaces.clear() - } - } + var addLineBreakAtBeginning = false + staticSurfaces.let { staticSurfaces -> + node.paintStatics(staticSurfaces, ansiLevel) + if (staticSurfaces.isNotEmpty()) { + staticSurfaces.forEach { staticSurface -> + appendSurface(staticSurface, addLineBreakAtBeginning) + if (!addLineBreakAtBeginning && staticSurface.height > 0) { + addLineBreakAtBeginning = true + } + } + staticSurfaces.clear() + } + } val surface = node.paint(ansiLevel) if (node.height > 0) { - appendSurface(surface, addLineBreakAtBeginning = afterStatic) + appendSurface(surface, addLineBreakAtBeginning) } lastHeight = surface.height diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt index e4608fd68..02b803a07 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/AnsiRenderingTest.kt @@ -169,11 +169,11 @@ class AnsiRenderingTest { assertThat(rendering.render(rootNode).toString()).isEqualTo( """ - |${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor - |Two $ansiClearLineAfterCursor + |${ansiMoveCursorToFirstColumn}One$ansiClearLineAfterCursor + |Two$ansiClearLineAfterCursor |Three$ansiClearLineAfterCursor - |Four $ansiClearLineAfterCursor - |Five $ansiClearLineAfterCursor + |Four$ansiClearLineAfterCursor + |Five$ansiClearLineAfterCursor |Sup$ansiClearLineAfterCursor$ansiClearAllAfterCursor """.trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(), ) diff --git a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt index 56691bdf9..94b271d2f 100644 --- a/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt +++ b/mosaic-runtime/src/commonTest/kotlin/com/jakewharton/mosaic/TestMosaicComposition.kt @@ -80,10 +80,10 @@ private class RealTestMosaicComposition( } else { rendering.render(rootNode).toString() .removeSurrounding(ansiBeginSynchronizedUpdate, ansiEndSynchronizedUpdate) - .replace(Regex("$ESC\\[\\d+A"), "") - .removePrefix(ansiMoveCursorToFirstColumn) - .removePrefix(ansiClearAllAfterCursor) - .replace(ansiClearLineAfterCursor, "") + .replace(Regex("$ESC\\[\\d+A"), "") + .removePrefix(ansiMoveCursorToFirstColumn) + .removeSuffix(ansiClearAllAfterCursor) + .replace(ansiClearLineAfterCursor, "") .replace("\r\n", "\n") // CRLF to LF for simplicity } renderSnapshots.trySend(stringRender) diff --git a/samples/rrtop/src/main/kotlin/example/RrtopApp.kt b/samples/rrtop/src/main/kotlin/example/RrtopApp.kt index f8851a2e4..075574cec 100644 --- a/samples/rrtop/src/main/kotlin/example/RrtopApp.kt +++ b/samples/rrtop/src/main/kotlin/example/RrtopApp.kt @@ -32,7 +32,7 @@ fun RrtopApp(rrtopViewModel: RrtopViewModel, colorsPalette: RrtopColorsPalette) CompositionLocalProvider(LocalRrtopColorsPalette provides colorsPalette) { Box( modifier = Modifier - .size(terminal.size) + .size(terminal.size) .background(LocalRrtopColorsPalette.current.mainBg) .onKeyEvent { when (it) {