diff --git a/README.md b/README.md index e70859f5..564fe0fc 100644 --- a/README.md +++ b/README.md @@ -1185,8 +1185,8 @@ wide as the second. If you have two star-sized columns both set to "2" then they ![Star grid example](https://github.com/varabyte/media/raw/main/kotter/images/kotter-grid-star-sized.png) To determine "remaining space", the `grid` method accepts a `targetWidth` parameter. If you don't have any star-sized -columns, the value does nothing. If you do, then the grid will subtract all fixed and fit width values from it and share -any remaining space between the star-sized columns. +columns, the `targetWidth` value does nothing. If you do, then the grid will subtract all fixed and fit width values +from it and share any remaining space between the star-sized columns. For a trivial example, say you have a two-column grid with `targetWidth` set to 10. The first column is fixed to 4, and the second column is set to star-sized. The star-sized column will then receive 6 characters of space. @@ -1196,46 +1196,36 @@ automatically adjust to the remaining space. If you do not set the `targetWidth` at all, then all star-sized columns will shrink to size 1. -#### Column specs and Cols.fromStr +#### Column builder Earlier, we used `Cols(6, 6)`, a convenience constructor that accepted only integer values indicated fixed widths. But -for more control, you can construct the `Cols` class passing in multiple `Cols.Spec` instances: +for more control, you can construct the `Cols` class using a builder block: ```kotlin -grid(Cols(Cols.Spec.Fit(), Cols.Spec.Fixed(10), Cols.Spec.Star()), targetWidth = 80) { +grid(Cols { fit(); fixed(10); star() }, targetWidth = 80) { /* ... */ } ``` -This works fine but is a bit verbose, so Kotter provides a convenient shortcut using a string value: - -```kotlin -grid(Cols.fromStr("fit, 10, *"), targetWidth = 80) { - /* ... */ -} -``` - -The string format is more fragile in that specifying it wrong will result in a runtime exception instead of a -compile-time one. However, it is more concise and easier to read. - #### Column properties -In addition to their base type, columns have a few properties you can set: *minimum value*, *maximum value*, and +In addition to their base value, columns have a few properties you can set: *minimum value*, *maximum value*, and *justification*. -If you construct a `Cols.Spec` directly, you can set these properties just by passing them in as constructor parameters. -However, if you use the `fromStr` format, you can specify them using a `key:value` (no spaces!) syntax. - Here's an example of setting all three properties: ```kotlin -grid(Cols.fromStr("fit max:10, 10 just:center, * min:5"), targetWidth = 80) { +grid( + Cols { + fit(maxValue = 10); fixed(10, justification = Justification.CENTER); star(minValue = 5) + }, targetWidth = 80 +) { /* ... */ } ``` The above means that the first column will be fit-sized, but will never exceed 10 characters. The second column is fixed -to 10 characters, and its contents will be centered. The final column is star-sized, but will never be less than 5 +to 10 characters, and its contents will be centered. The final column is star-sized, but it will never be less than 5 characters. ### 🪝 Shutdown Hook diff --git a/examples/grid/src/main/kotlin/main.kt b/examples/grid/src/main/kotlin/main.kt index 97e4c55a..e8d2f9cb 100644 --- a/examples/grid/src/main/kotlin/main.kt +++ b/examples/grid/src/main/kotlin/main.kt @@ -2,8 +2,9 @@ import com.varabyte.kotter.foundation.* import com.varabyte.kotter.foundation.input.* import com.varabyte.kotter.foundation.text.* import com.varabyte.kotterx.grid.* +import com.varabyte.kotterx.text.* -private const val MIN_TABLE_WIDTH = 20 +private const val MIN_TABLE_WIDTH = 25 private const val MAX_TABLE_WIDTH = 40 private const val DEFAULT_TABLE_WIDTH = 30 @@ -26,7 +27,7 @@ fun main() = session { textLine("Target width: $tableWidth") grid( - Cols.fromStr("fit, 7 just:center, * min:5"), + Cols { fit(); fixed(10, Justification.CENTER); star(minWidth = 5)}, targetWidth = tableWidth, characters = GridCharacters.CURVED, paddingLeftRight = if (usePadding) 1 else 0, diff --git a/kotter/src/commonMain/kotlin/com/varabyte/kotterx/grid/GridSupport.kt b/kotter/src/commonMain/kotlin/com/varabyte/kotterx/grid/GridSupport.kt index f703896d..7b22ca0f 100644 --- a/kotter/src/commonMain/kotlin/com/varabyte/kotterx/grid/GridSupport.kt +++ b/kotter/src/commonMain/kotlin/com/varabyte/kotterx/grid/GridSupport.kt @@ -85,7 +85,7 @@ class GridCharacters( /** * A column specification for a grid. */ -class Cols(vararg val specs: Spec) { +class Cols private constructor(internal vararg val specs: Spec) { init { fun assertPositive(value: Int?, name: String) { if (value != null) require(value > 0) { "$name must be positive" } @@ -117,7 +117,7 @@ class Cols(vararg val specs: Spec) { constructor(vararg widths: Int) : this(*widths.map { Spec.Fixed(it) }.toTypedArray()) - sealed class Spec(val justification: Justification?) { + internal sealed class Spec(val justification: Justification?) { class Fit(justification: Justification? = null, val minWidth: Int? = null, val maxWidth: Int? = null) : Spec(justification) @@ -131,6 +131,24 @@ class Cols(vararg val specs: Spec) { ) : Spec(justification) } + class BuilderScope { + private val specs = mutableListOf() + + fun fit(justification: Justification? = null, minWidth: Int? = null, maxWidth: Int? = null) { + specs.add(Spec.Fit(justification, minWidth, maxWidth)) + } + + fun fixed(width: Int, justification: Justification? = null) { + specs.add(Spec.Fixed(width, justification)) + } + + fun star(ratio: Int = 1, justification: Justification? = null, minWidth: Int? = null, maxWidth: Int? = null) { + specs.add(Spec.Star(ratio, justification, minWidth, maxWidth)) + } + + fun build() = Cols(*specs.toTypedArray()) + } + companion object { /** * Create a column spec where all columns are the same width. @@ -138,110 +156,12 @@ class Cols(vararg val specs: Spec) { fun uniform(count: Int, width: Int) = Cols(*IntArray(count) { width }) /** - * Parse a string which represents [Cols.Spec] values and use it to create a [Cols] instance. - * - * In other words, this is an efficient (but less type-safe) way to create a list of grid column specs, with - * concise syntax for fixed-, fit-, and star-sized columns. - * - * For example: `Cols.fromStr("fit, 2*, 20, *")` - * - * Fit-sizing means that the column will take up exactly the space it needs to contain the largest item in that - * column. Use the "fit" keyword to specify this. - * - * Star-sizing means that the column will take up the remaining space in the grid. If multiple different star - * sections exist, they will be divided up evenly based on their ratio. For example, with "2*, *", the first - * column will be twice as wide as the second. - * - * And fixed-sizing means that the column will take up exactly the specified width. Use a raw integer value to - * specify this. - * - * You can mix and match types. So "fit, 2*, 20, *" means: subtract the fit size of the first column and the - * fixed size of the third column, then divide any remaining space up between the star-sized columns. Let's say - * the target width of the grid is 50, and the fit column ended up getting calculated as 6, then the final sizes - * would be: [6, 16, 20, 8]. - * - * Additional properties can also be specified, using a `key:value` syntax. For example, `fit min:5 max:10` - * means calculate a fit value for this column, but go no lower than 5 and no higher than 10. Fit and star-sized - * columns can both have min- and max-width properties. All columns can specify a `just` property which can be - * set to `left`, `center`, or `right`. - * - * | Name | Values | Applies to | - * | ---- | ------ | ---------- | - * | min | int | fit, star | - * | max | int | fit, star | - * | just | left, center, right | all | - * - * For example: `Cols.fromStr("fit min:5, 20 just: center, * max: 30")` + * Convenience method for constructing a [Cols] instance using a builder pattern. */ - fun fromStr(str: String): Cols { - fun invalidPartMessage(part: String, extra: String? = null) = - "Invalid column spec: $part" + (extra?.let { " ($it)" } ?: "") - - class ParsedPart(val value: String, val properties: Map) { - private val part get() = "$value ${properties.entries.joinToString(" ") { (k, v) -> "$k:$v" }}" - - val maxWidth = properties["max"]?.toIntOrNull() - val minWidth = properties["min"]?.toIntOrNull() - val justification = properties["just"]?.let { justStr -> - Justification.values().find { it.name.equals(justStr, ignoreCase = true) } - ?: error(invalidPartMessage( - part, - "Invalid justification value \"$justStr\", should be one of [${ - Justification.values().joinToString(", ") { it.name.lowercase() } - }]" - )) - } - } - - fun parsePart(part: String): ParsedPart { - val parts = part.split(" ") - val value = parts.first() - val properties = parts.drop(1).map { it.split(":") }.associate { it[0] to it[1] } - return ParsedPart(value, properties) - } - - val specs: List = str - .split(",") - .map { it.trim() } - .map { part -> - val parsedPart = parsePart(part) - - when { - parsedPart.value.equals("fit", ignoreCase = true) -> { - Spec.Fit( - parsedPart.justification, - parsedPart.minWidth, - parsedPart.maxWidth - ) - } - - parsedPart.value.endsWith("*") -> { - val ratio = parsedPart.value.dropLast(1).let { - if (it.isEmpty()) 1 else it.toIntOrNull() - } - require(ratio != null) { invalidPartMessage(part, "Invalid star size") } - Spec.Star( - ratio, - parsedPart.justification, - parsedPart.minWidth, - parsedPart.maxWidth - ) - } - - else -> { - val width = parsedPart.value.toIntOrNull() - require(width != null && width > 0) { - invalidPartMessage( - part, - "Column width must be positive" - ) - } - Spec.Fixed(width, parsedPart.justification) - } - } - } - - return Cols(*specs.toTypedArray()) + operator fun invoke(builder: BuilderScope.() -> Unit): Cols { + val scope = BuilderScope() + scope.builder() + return scope.build() } } } diff --git a/kotter/src/commonTest/kotlin/com/varabyte/kotterx/grid/GridSupportTest.kt b/kotter/src/commonTest/kotlin/com/varabyte/kotterx/grid/GridSupportTest.kt index 99810087..02bcf22e 100644 --- a/kotter/src/commonTest/kotlin/com/varabyte/kotterx/grid/GridSupportTest.kt +++ b/kotter/src/commonTest/kotlin/com/varabyte/kotterx/grid/GridSupportTest.kt @@ -155,7 +155,7 @@ class GridSupportTest { @Test fun `Cols fromStr works with dynamic sizing`() = testSession { terminal -> section { - grid(cols = Cols.fromStr("3*, 4, 1*"), targetWidth = 12) { + grid(cols = Cols { star(3); fixed(4); star(1) }, targetWidth = 12) { cell { textLine("A") } @@ -184,7 +184,7 @@ class GridSupportTest { // ... plus padding of 1 on each side -> padded size (8, 5, 11) // ... plus border walls (4 of them) -> final total width = 8 + 5 + 11 + 4 = 28 - grid(cols = Cols.fromStr("2*, *, 3*"), paddingLeftRight = 1, targetWidth = 18) { + grid(cols = Cols { star(2); star(); star(3) }, paddingLeftRight = 1, targetWidth = 18) { cell { textLine("A") } @@ -208,7 +208,7 @@ class GridSupportTest { @Test fun `fit sizing works`() = testSession { terminal -> section { - grid(cols = Cols.fromStr("fit, fit, fit")) { + grid(cols = Cols { fit(); fit(); fit() }) { cell { textLine("A") } @@ -250,7 +250,7 @@ class GridSupportTest { @Test fun `fit sizing takes newlines into account`() = testSession { terminal -> section { - grid(cols = Cols.fromStr("fit")) { + grid(cols = Cols { fit() }) { cell { textLine("X") } @@ -281,7 +281,7 @@ class GridSupportTest { // - Grid default grid( - Cols.fromStr("8, 8, 8 just:right"), + Cols { fixed(8); fixed(8); fixed(8, justification = Justification.RIGHT) }, paddingLeftRight = 1, defaultJustification = Justification.CENTER ) { @@ -354,7 +354,7 @@ class GridSupportTest { @Test fun `star widths without target width shrink to size 1`() = testSession { terminal -> section { - grid(Cols.fromStr("*, 10*")) { + grid(Cols { star(); star(10) }) { cell { textLine("AA") } cell { textLine("BB") } } @@ -372,7 +372,7 @@ class GridSupportTest { @Test fun `maxCellHeight can be used to limit number of cell rows`() = testSession { terminal -> section { - grid(Cols.fromStr("1, 1, 1, 1"), maxCellHeight = 2) { + grid(Cols.uniform(4, width = 1), maxCellHeight = 2) { cell { textLine("A") } cell { textLine("BB") } cell { textLine("CCC") } @@ -397,9 +397,9 @@ class GridSupportTest { } @Test - fun `columns can set min and max widths`() = testSession { terminal -> + fun `non-fixed columns can set min and max widths`() = testSession { terminal -> section { - grid(cols = Cols.fromStr("* min:5, fit max:5"), targetWidth = 1) { + grid(cols = Cols { star(minWidth = 5); fit(maxWidth = 5) }, targetWidth = 1) { cell { textLine("A") } @@ -417,19 +417,4 @@ class GridSupportTest { Ansi.Csi.Codes.Sgr.RESET.toFullEscapeCode(), ).inOrder() } - - - @Test - fun `non-integer star widths fails`() = testSession { - section { - assertThrows { - grid(Cols.fromStr("1.5*")) { - cell { textLine("A") } - cell { textLine("B") } - } - }.also { ex -> - assertThat(ex.message!!).contains("1.5*") - } - }.run() - } }