Skip to content

No last empty line #505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
@file:Suppress("NOTHING_TO_INLINE")

package com.jakewharton.mosaic

import com.github.ajalt.mordant.rendering.AnsiLevel as MordantAnsiLevel
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 ansiClearLineAfterCursor = "${CSI}K"
internal const val ansiMoveCursorToFirstColumn = "${CSI}0G"
internal const val ansiClearAllAfterCursor = "${CSI}0J"

internal const val ansiReset = "${CSI}0"
internal const val clearLine = "${CSI}K"
internal const val cursorUp = "${CSI}F"

internal const val cursorHide = "$CSI?25l"
internal const val cursorShow = "$CSI?25h"
Expand All @@ -32,6 +36,12 @@ internal const val ansiBgColorOffset = 10
internal const val ansiSelectorColor256 = 5
internal const val ansiSelectorColorRgb = 2

internal inline fun StringBuilder.ansiMoveCursorUp(lines: Int) {
append(CSI)
append(lines)
append("A")
}

internal fun MordantAnsiLevel.toMosaicAnsiLevel(): AnsiLevel {
return when (this) {
MordantAnsiLevel.NONE -> AnsiLevel.NONE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextSurface>()
private var lastHeight = 0

Expand All @@ -81,51 +81,44 @@ 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")
}
// don't need to move cursor up if there was zero or one line
if (lastHeight > 1) {
ansiMoveCursorUp(lastHeight - 1)
}
append(ansiMoveCursorToFirstColumn)

var addLineBreakAtBeginning = false
staticSurfaces.let { staticSurfaces ->
node.paintStatics(staticSurfaces, ansiLevel)
if (staticSurfaces.isNotEmpty()) {
staticSurfaces.forEach { staticSurface ->
appendSurface(staticSurface)
appendSurface(staticSurface, addLineBreakAtBeginning)
if (!addLineBreakAtBeginning && staticSurface.height > 0) {
addLineBreakAtBeginning = 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)
}
lastHeight = surface.height

append(ansiClearAllAfterCursor)
append(ansiEndSynchronizedUpdate)
}
}

lastHeight = surface.height
private fun StringBuilder.appendSurface(canvas: TextSurface, addLineBreakAtBeginning: Boolean) {
for (rowIndex in 0 until canvas.height) {
if (rowIndex > 0 || (rowIndex == 0 && addLineBreakAtBeginning)) {
append("\r\n")
}
canvas.appendRowTo(this, rowIndex)
append(ansiClearLineAfterCursor)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ class AnsiRenderingTest {
// TODO We should not draw trailing whitespace.
assertThat(rendering.render(rootNode).toString()).isEqualTo(
"""
|Hello$s
|World!
|
|${ansiMoveCursorToFirstColumn}Hello $ansiClearLineAfterCursor
|World!$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand All @@ -40,9 +39,8 @@ class AnsiRenderingTest {

assertThat(rendering.render(firstRootNode).toString()).isEqualTo(
"""
|Hello$s
|World!
|
|${ansiMoveCursorToFirstColumn}Hello $ansiClearLineAfterCursor
|World!$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)

Expand All @@ -57,11 +55,10 @@ class AnsiRenderingTest {

assertThat(rendering.render(secondRootNode).toString()).isEqualTo(
"""
|$cursorUp${cursorUp}Hel$clearLine
|lo $clearLine
|Wor
|ld!
|
|${ansiMoveCursorUp(1)}${ansiMoveCursorToFirstColumn}Hel$ansiClearLineAfterCursor
|lo $ansiClearLineAfterCursor
|Wor$ansiClearLineAfterCursor
|ld!$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand All @@ -78,11 +75,10 @@ class AnsiRenderingTest {

assertThat(rendering.render(firstRootNode).toString()).isEqualTo(
"""
|Hel
|lo$s
|Wor
|ld!
|
|${ansiMoveCursorToFirstColumn}Hel$ansiClearLineAfterCursor
|lo $ansiClearLineAfterCursor
|Wor$ansiClearLineAfterCursor
|ld!$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)

Expand All @@ -95,10 +91,8 @@ class AnsiRenderingTest {

assertThat(rendering.render(secondRootNode).toString()).isEqualTo(
"""
|$cursorUp$cursorUp$cursorUp${cursorUp}Hello $clearLine
|World!$clearLine
|$clearLine
|$clearLine$cursorUp
|${ansiMoveCursorUp(3)}${ansiMoveCursorToFirstColumn}Hello $ansiClearLineAfterCursor
|World!$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand All @@ -113,9 +107,8 @@ class AnsiRenderingTest {

assertThat(rendering.render(rootNode).toString()).isEqualTo(
"""
|World!
|Hello
|
|${ansiMoveCursorToFirstColumn}World!$ansiClearLineAfterCursor
|Hello$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand All @@ -130,9 +123,8 @@ class AnsiRenderingTest {

assertThat(rendering.render(firstRootNode).toString()).isEqualTo(
"""
|One
|Two
|
|${ansiMoveCursorToFirstColumn}One$ansiClearLineAfterCursor
|Two$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)

Expand All @@ -145,9 +137,8 @@ class AnsiRenderingTest {

assertThat(rendering.render(secondRootNode).toString()).isEqualTo(
"""
|${cursorUp}Three$clearLine
|Four
|
|${ansiMoveCursorToFirstColumn}Three$ansiClearLineAfterCursor
|Four$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand Down Expand Up @@ -178,13 +169,12 @@ class AnsiRenderingTest {

assertThat(rendering.render(rootNode).toString()).isEqualTo(
"""
|One
|Two
|Three
|Four
|Five
|Sup
|
|${ansiMoveCursorToFirstColumn}One$ansiClearLineAfterCursor
|Two$ansiClearLineAfterCursor
|Three$ansiClearLineAfterCursor
|Four$ansiClearLineAfterCursor
|Five$ansiClearLineAfterCursor
|Sup$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand All @@ -204,10 +194,9 @@ class AnsiRenderingTest {

assertThat(rendering.render(firstRootNode).toString()).isEqualTo(
"""
|Static
|TopTopTop
|LeftLeft$s
|
|${ansiMoveCursorToFirstColumn}Static$ansiClearLineAfterCursor
|TopTopTop$ansiClearLineAfterCursor
|LeftLeft $ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
"${ansiMoveCursorToFirstColumn}The count is: ${i}$ansiClearLineAfterCursor$ansiClearAllAfterCursor".wrapWithAnsiSynchronizedUpdate(),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ class MosaicTest {
}
assertThat(actual).isEqualTo(
"""
|One $s
|Two $s
|Three
|
|${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor
|Two $ansiClearLineAfterCursor
|Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand All @@ -75,10 +74,9 @@ class MosaicTest {
}
assertThat(actual).isEqualTo(
"""
|One $s
|Two $s
|Three
|
|${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor
|Two $ansiClearLineAfterCursor
|Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand All @@ -105,10 +103,9 @@ class MosaicTest {
}
assertThat(actual).isEqualTo(
"""
|One $s
|Two $s
|Three
|
|${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor
|Two $ansiClearLineAfterCursor
|Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand Down Expand Up @@ -147,10 +144,9 @@ class MosaicTest {
}
assertThat(actual).isEqualTo(
"""
|One $s
|Two $s
|Three
|
|${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor
|Two $ansiClearLineAfterCursor
|Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand All @@ -167,10 +163,9 @@ class MosaicTest {
}
assertThat(actual).isEqualTo(
"""
|One $s
|Two $s
|Three
|
|${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor
|Two $ansiClearLineAfterCursor
|Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand All @@ -191,10 +186,9 @@ class MosaicTest {
actuals.forEach { actual ->
assertThat(actual).isEqualTo(
"""
|One $s
|Two $s
|Three
|
|${ansiMoveCursorToFirstColumn}One $ansiClearLineAfterCursor
|Two $ansiClearLineAfterCursor
|Three$ansiClearLineAfterCursor$ansiClearAllAfterCursor
""".trimMargin().wrapWithAnsiSynchronizedUpdate().replaceLineEndingsWithCRLF(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ 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)
.removeSuffix(ansiClearAllAfterCursor)
.replace(ansiClearLineAfterCursor, "")
.replace("\r\n", "\n") // CRLF to LF for simplicity
}
renderSnapshots.trySend(stringRender)
Expand Down
Loading