Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ajalt/mordant
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 2.0.0-beta6
Choose a base ref
...
head repository: ajalt/mordant
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 2.0.0-beta7
Choose a head ref
  • 18 commits
  • 48 files changed
  • 1 contributor

Commits on May 22, 2022

  1. Copy the full SHA
    220f7f8 View commit details
  2. Copy the full SHA
    53920ab View commit details
  3. Copy the full SHA
    8cee203 View commit details

Commits on May 30, 2022

  1. Add more tests for tableBorders

    ajalt committed May 30, 2022
    Copy the full SHA
    36ce77a View commit details
  2. Copy the full SHA
    92f4903 View commit details

Commits on Jun 11, 2022

  1. Add Prompt class

    ajalt committed Jun 11, 2022
    Copy the full SHA
    74315b9 View commit details
  2. Add choices support to prompt

    ajalt committed Jun 11, 2022
    Copy the full SHA
    94c018a View commit details
  3. Add docs

    ajalt committed Jun 11, 2022
    Copy the full SHA
    6edd16e View commit details
  4. Update gitignore

    ajalt committed Jun 11, 2022
    Copy the full SHA
    5b40970 View commit details
  5. Copy the full SHA
    f2f4c06 View commit details
  6. Add YesNoPrompt

    ajalt committed Jun 11, 2022
    Copy the full SHA
    c412216 View commit details
  7. Fix warnings

    ajalt committed Jun 11, 2022
    Copy the full SHA
    3e70f31 View commit details
  8. Split stdout and stderr in VTI

    ajalt committed Jun 11, 2022
    Copy the full SHA
    280c13e View commit details
  9. Add html renderer

    ajalt committed Jun 11, 2022
    Copy the full SHA
    c38ba58 View commit details
  10. Copy the full SHA
    e1dbc42 View commit details
  11. Update readme

    ajalt committed Jun 11, 2022
    Copy the full SHA
    9bed746 View commit details
  12. Update Kotlin to 1.7.0

    ajalt committed Jun 11, 2022
    Copy the full SHA
    6f58d55 View commit details

Commits on Jun 12, 2022

  1. Release version 2.0.0-beta7

    ajalt committed Jun 12, 2022
    Copy the full SHA
    d7026d5 View commit details
Showing with 1,611 additions and 804 deletions.
  1. +1 −0 .gitignore
  2. +16 −0 CHANGELOG.md
  3. +21 −1 README.md
  4. +1 −1 gradle.properties
  5. +1 −1 gradle/wrapper/gradle-wrapper.properties
  6. +2 −0 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/MppH.kt
  7. +2 −2 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/internal/Parsing.kt
  8. +38 −21 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/BorderType.kt
  9. +5 −0 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Theme.kt
  10. +59 −28 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/Table.kt
  11. +2 −2 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableCsv.kt
  12. +23 −7 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDsl.kt
  13. +16 −2 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDslInstances.kt
  14. +10 −4 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableLayout.kt
  15. +71 −0 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/HtmlRenderer.kt
  16. +237 −0 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/Prompt.kt
  17. +3 −0 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/StdoutTerminalInterface.kt
  18. +91 −8 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/Terminal.kt
  19. +9 −0 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalInterface.kt
  20. +118 −0 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalRecorder.kt
  21. +0 −42 mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/VirtualTerminalInterface.kt
  22. +4 −4 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/markdown/MarkdownTest.kt
  23. +44 −44 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextAlignmentTest.kt
  24. +17 −17 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextOverflowWrapTest.kt
  25. +60 −48 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextWhitespaceTest.kt
  26. +210 −210 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableBorderStyleTest.kt
  27. +33 −35 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableBorderTest.kt
  28. +1 −1 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableColumnWidthTest.kt
  29. +194 −149 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableTest.kt
  30. +26 −0 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/HtmlRendererTest.kt
  31. +73 −0 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/PromptTest.kt
  32. +12 −12 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalCursorTest.kt
  33. +9 −9 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalTest.kt
  34. +2 −2 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt
  35. +49 −49 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/DefinitionListTest.kt
  36. +2 −2 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/HorizontalRuleTest.kt
  37. +30 −30 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/PaddingTest.kt
  38. +34 −34 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/PanelTest.kt
  39. +12 −12 mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/TextTest.kt
  40. +29 −4 mordant/src/jsMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt
  41. +11 −0 mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt
  42. +14 −14 mordant/src/jvmTest/kotlin/com/github/ajalt/mordant/animation/ProgressAnimationTest.kt
  43. +5 −0 mordant/src/nativeMain/kotlin/com/github/ajalt/mordant/internal/MppImpl.kt
  44. +1 −0 samples/markdown/build.gradle.kts
  45. +2 −0 samples/progress/build.gradle.kts
  46. +1 −0 samples/table/build.gradle.kts
  47. +5 −6 samples/table/src/commonMain/kotlin/com/github/ajalt/mordant/samples/main.kt
  48. +5 −3 settings.gradle.kts
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -11,3 +11,4 @@ docs/api/
docs/changelog.md
docs/index.md
site/
kotlin-js-store/
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 2.0.0-beta7
### Added
- Functionality for reading user input: `Terminal.readLineOrNull`, `Terminal.prompt` and various `Prompt` classes
- `TerminalRecorder` that saves output to memory rather than printing it.
- `TerminalRecorder.outputAsHtml()` that can render recorded output as an html file.
-
### Changed
- When building tables, `borders` has been renamed `cellBorders`, and `outerBorder: Boolean` has been replaced with `tableBorders: Borders?`, which allows more control over the table's outside borders. [(#58)](https://github.com/ajalt/mordant/issues/58)
- Update Kotlin to 1.7.0

### Fixed
- Avoid clobbering output when using `Terminal.forStdErr` while an animation is running. [(#54)](https://github.com/ajalt/mordant/issues/54)

### Deprecated
- Deprecated the `VirtualTerminalInterface`. Use `TerminalRecorder` instead.

## 2.0.0-beta6
### Changed
- Update Kotlin to 1.6.20
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -234,13 +234,33 @@ manually.
Call `progress.start` to animate the progress, and `progress.update` or `progress.advance` as your
task completes.

## Prompting for input

You can ask the user to enter text and wait for a response with `Terminal.prompt`:

```kotlin
val t = Terminal()
val response = t.prompt("Choose a size", choices=listOf("small", "large"))
t.println("You chose: $response")
```

```text
$ ./example
Choose a size [small, large]: small
You chose: small
```

You can customize the prompt behavior further or convert the response to other types
creating a subclass of the `Prompt` class.


## Installation

Mordant is distributed through Maven Central.

```groovy
dependencies {
implementation("com.github.ajalt.mordant:mordant:2.0.0-beta6")
implementation("com.github.ajalt.mordant:mordant:2.0.0-beta7")
}
```

2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION_NAME=2.0.0-beta6
VERSION_NAME=2.0.0-beta7

kotlin.mpp.stability.nowarn=true
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
@@ -33,6 +33,8 @@ internal expect fun makePrintingTerminalCursor(terminal: Terminal): TerminalCurs

internal expect fun printStderr(message: String, newline: Boolean)

internal expect fun readLineOrNullMpp(hideInput: Boolean): String?

@OptIn(ExperimentalTerminalApi::class)
internal expect fun sendInterceptedPrintRequest(
request: PrintRequest,
Original file line number Diff line number Diff line change
@@ -17,8 +17,8 @@ private data class Chunk(val text: String, val style: TextStyle)
*
* Unknown ANSI codes are discarded.
*/
internal fun parseText(text: String, style: TextStyle): Lines {
val parseAnsi = parseAnsi(text, style)
internal fun parseText(text: String, defaultStyle: TextStyle): Lines {
val parseAnsi = parseAnsi(text, defaultStyle)
val words = parseAnsi.flatMap { splitWords(it) }.toList()
val splitLines = splitLines(words)
return Lines(splitLines)
Original file line number Diff line number Diff line change
@@ -3,38 +3,55 @@ package com.github.ajalt.mordant.rendering
import com.github.ajalt.mordant.internal.DEFAULT_STYLE


/**
* Characters to use for one section of a [BorderType]
*/
class BorderTypeSection(
private val corners: String,
val es: String,
val esw: String,
val sw: String,
val nes: String,
val nesw: String,
val nsw: String,
val ne: String,
val new: String,
val nw: String,
val ew: String,
val ns: String,
val s: String,
val n: String,
val w: String,
val e: String,
) {
init {
require(corners.length == 15) { "string of corners must have length==15" }
}
constructor(corners: String) : this(
es = corners[0].toString(),
esw = corners[1].toString(),
sw = corners[2].toString(),
nes = corners[3].toString(),
nesw = corners[4].toString(),
nsw = corners[5].toString(),
ne = corners[6].toString(),
new = corners[7].toString(),
nw = corners[8].toString(),
ew = corners[9].toString(),
ns = corners[10].toString(),
s = corners[11].toString(),
n = corners[12].toString(),
w = corners[13].toString(),
e = corners[14].toString(),
)

// Indexing into this array is ~2x faster than if we indexed into `corners` directly.
private val array = arrayOf(" ", w, s, sw, e, ew, es, esw, n, nw, ns, nsw, ne, new, nes, nesw)

val es: String get() = corners[0].toString()
val esw: String get() = corners[1].toString()
val sw: String get() = corners[2].toString()
val nes: String get() = corners[3].toString()
val nesw: String get() = corners[4].toString()
val nsw: String get() = corners[5].toString()
val ne: String get() = corners[6].toString()
val new: String get() = corners[7].toString()
val nw: String get() = corners[8].toString()
val ew: String get() = corners[9].toString()
val ns: String get() = corners[10].toString()
val s: String get() = corners[11].toString()
val n: String get() = corners[12].toString()
val w: String get() = corners[13].toString()
val e: String get() = corners[14].toString()

fun getCorner(n: Boolean, e: Boolean, s: Boolean, w: Boolean, textStyle: TextStyle = DEFAULT_STYLE): Span {
val i = (if (n) 8 else 0) or (if (e) 4 else 0) or (if (s) 2 else 0) or (if (w) 1 else 0)
return Span.word(array[i], textStyle)
}
}

/**
* Characters to use to draw borders on a table or other box widget.
*/
class BorderType(
val head: BorderTypeSection,
val headBottom: BorderTypeSection,
Original file line number Diff line number Diff line change
@@ -30,6 +30,11 @@ sealed class Theme(
"hr.rule" to DEFAULT_STYLE,
"panel.border" to DEFAULT_STYLE,

"prompt.prompt" to DEFAULT_STYLE,
"prompt.default" to TextStyle(DEFAULT_HIGHLIGHT, bold = true),
"prompt.choices" to TextStyle(DEFAULT_HIGHLIGHT, bold = true),
"prompt.choices.invalid" to TextStyle(DEFAULT_RED),

"progressbar.pending" to TextStyle(DEFAULT_GRAY),
"progressbar.complete" to TextStyle(DEFAULT_HIGHLIGHT),
"progressbar.indeterminate" to TextStyle(DEFAULT_HIGHLIGHT),
Original file line number Diff line number Diff line change
@@ -74,22 +74,34 @@ internal class TableImpl(
val headerRowCount: Int,
val footerRowCount: Int,
val columnStyles: Map<Int, ColumnWidth>,
val outerBorder: Boolean,
val tableBorders: Borders?,
) : Table() {
init {
require(rows.isNotEmpty()) { "Table cannot be empty" }
}

private val expand = columnStyles.values.any { it is ColumnWidth.Expand }
private val columnCount = rows.maxOf { it.size }

/** Whether any cell in row `i` has a border above it */
private val rowBorders = List(rows.size + 1) { y ->
(outerBorder || y in 1 until rows.size) && (0 until columnCount).any { x ->
getCell(x, y)?.borderTop == true || getCell(x, y - 1)?.borderBottom == true
when {
y == 0 && tableBorders != null -> tableBorders.top
y == rows.size && tableBorders != null -> tableBorders.bottom
else -> (0 until columnCount).any { x ->
getCell(x, y).t || getCell(x, y - 1).b
}
}
}

/** Whether any cell in column `i` has a border to its left */
private val columnBorders = List(columnCount + 1) { x ->
(outerBorder || x in 1 until columnCount) && rows.indices.any { y ->
getCell(x, y)?.borderLeft == true || getCell(x - 1, y)?.borderRight == true
when {
x == 0 && tableBorders != null -> tableBorders.left
x == columnCount && tableBorders != null -> tableBorders.right
else -> rows.indices.any { y ->
getCell(x, y).l || getCell(x - 1, y).r
}
}
}
private val borderWidth = columnBorders.count { it }
@@ -116,6 +128,7 @@ internal class TableImpl(
columnWidths = calculateColumnWidths(t, width),
columnBorders = columnBorders,
rowBorders = rowBorders,
tableBorders = tableBorders,
t = t
).render()
}
@@ -204,16 +217,17 @@ internal class TableImpl(
}

private class TableRenderer(
val rows: List<ImmutableRow>,
val borderType: BorderType,
val borderStyle: TextStyle,
val headerRowCount: Int,
val footerRowCount: Int,
val columnCount: Int,
val columnWidths: List<Int>,
val columnBorders: List<Boolean>,
val rowBorders: List<Boolean>,
val t: Terminal,
private val rows: List<ImmutableRow>,
private val borderType: BorderType,
private val borderStyle: TextStyle,
private val headerRowCount: Int,
private val footerRowCount: Int,
private val columnCount: Int,
private val columnWidths: List<Int>,
private val columnBorders: List<Boolean>,
private val rowBorders: List<Boolean>,
private val tableBorders: Borders?,
private val t: Terminal,
) {
private val rowCount get() = rows.size
private val renderedRows = rows.map { r ->
@@ -290,9 +304,17 @@ private class TableRenderer(
/** Return 1 if any cell in row [y] has a top border, or 0 if they don't */
private fun drawTopBorderForCell(tableLineY: Int, x: Int, y: Int, colWidth: Int, borderTop: Boolean?): Int {
if (!rowBorders[y]) return 0
if (colWidth == 0 || borderTop == null) return 1

val char = if (borderTop || cellAt(x, y - 1)?.borderBottom == true) sectionOfRow(y).ew else " "
if (colWidth == 0 || borderTop == null) {
// no char to draw here, but return 1 since some cell in this row has a top border
return 1
}

val char = if (
borderTop || y == 0 && tableBorders.t || y == rowCount && tableBorders.b || cellAt(x, y - 1).b
) {
sectionOfRow(y).ew
} else " "
tableLines[tableLineY].add(Span.word(char.repeat(colWidth), borderStyle))
return 1
}
@@ -313,12 +335,11 @@ private class TableRenderer(
val topBorderHeight = if (rowBorders[y]) 1 else 0

if (borderLeft != null) {
val border = when {
borderLeft || cellAt(x - 1, y)?.borderRight == true -> {
Span.word(sectionOfRow(y, allowBottom = false).ns, borderStyle)
}
else -> SINGLE_SPACE
}
val border = if (
x == 0 && tableBorders.l || x == columnCount && tableBorders.r || borderLeft || cellAt(x - 1, y).r
) {
Span.word(sectionOfRow(y, allowBottom = false).ns, borderStyle)
} else SINGLE_SPACE
for (i in 0 until rowHeight) {
tableLines[tableLineY + i + topBorderHeight].add(border)
}
@@ -369,11 +390,11 @@ private class TableRenderer(
return null
}
return sectionOfRow(y).getCorner(
tl?.borderRight == true || tr?.borderLeft == true,
tr?.borderBottom == true || br?.borderTop == true,
bl?.borderRight == true || br?.borderLeft == true,
tl?.borderBottom == true || bl?.borderTop == true,
borderStyle
n = tl.r || tr.l || y > 0 && (x == 0 && tableBorders.l || x == columnCount && tableBorders.r),
e = tr.b || br.t || x < columnCount && (y == 0 && tableBorders.t || y == rowCount && tableBorders.b),
s = bl.r || br.l || y < rowCount && (x == 0 && tableBorders.l || x == columnCount && tableBorders.r),
w = tl.b || bl.t || x > 0 && (y == 0 && tableBorders.t || y == rowCount && tableBorders.b),
textStyle = borderStyle
)
}

@@ -387,3 +408,13 @@ private class TableRenderer(
}
}
}

private val Borders?.l get() = this?.left == true
private val Borders?.r get() = this?.right == true
private val Borders?.t get() = this?.top == true
private val Borders?.b get() = this?.bottom == true

private val Cell?.l get() = this?.borderLeft == true
private val Cell?.r get() = this?.borderRight == true
private val Cell?.t get() = this?.borderTop == true
private val Cell?.b get() = this?.borderBottom == true
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import com.github.ajalt.mordant.rendering.AnsiLevel
import com.github.ajalt.mordant.rendering.TextAlign
import com.github.ajalt.mordant.terminal.ExperimentalTerminalApi
import com.github.ajalt.mordant.terminal.Terminal
import com.github.ajalt.mordant.terminal.VirtualTerminalInterface
import com.github.ajalt.mordant.terminal.TerminalRecorder
import com.github.ajalt.mordant.widgets.Padded
import com.github.ajalt.mordant.widgets.Text
import com.github.ajalt.mordant.widgets.withAlign
@@ -95,7 +95,7 @@ private fun Table.getContentRows(): List<List<String>> {
}

val t = Terminal(
terminalInterface = VirtualTerminalInterface(
terminalInterface = TerminalRecorder(
ansiLevel = AnsiLevel.NONE,
width = Int.MAX_VALUE,
height = Int.MAX_VALUE,
Loading