From 941a636b42e02c77126a0d085d4fe992276051bd Mon Sep 17 00:00:00 2001 From: AJ Alt Date: Mon, 2 Sep 2024 17:50:30 +0000 Subject: [PATCH] Add nonInteractiveWidth override (#223) --- CHANGELOG.md | 1 + mordant/api/mordant.api | 4 +-- .../github/ajalt/mordant/terminal/Terminal.kt | 32 +++++++++++++++++-- .../mordant/terminal/TerminalDetection.kt | 18 +++++++++-- .../ajalt/mordant/terminal/TerminalTest.kt | 21 ++++++++++++ .../ajalt/mordant/test/RenderingTest.kt | 9 +++++- 6 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eed16680d..d7350b705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Added tvOS and watchOS native targets to all modules except the new `mordant-markdown` module. - Added ability to control raw mode with the `TerminalRecorder`. - Added support for unicode input in raw mode. +- Added `nonInteractiveWidth` and `nonInteractiveHeight` to `Terminal` terminal constructor to set a different width when the terminal is not interactive (e.g. when redirecting output to a file) [(#140)](https://github.com/ajalt/mordant/issues/140) ### Changed - **Breaking Change** Moved `Terminal.info.width` and `height` to `Terminal.size.width` and `height`. diff --git a/mordant/api/mordant.api b/mordant/api/mordant.api index afb0e1c6d..8742dca8d 100644 --- a/mordant/api/mordant.api +++ b/mordant/api/mordant.api @@ -1186,8 +1186,8 @@ public final class com/github/ajalt/mordant/terminal/StringPrompt : com/github/a } public final class com/github/ajalt/mordant/terminal/Terminal { - public fun (Lcom/github/ajalt/mordant/rendering/AnsiLevel;Lcom/github/ajalt/mordant/rendering/Theme;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;ILjava/lang/Boolean;Lcom/github/ajalt/mordant/terminal/TerminalInterface;)V - public synthetic fun (Lcom/github/ajalt/mordant/rendering/AnsiLevel;Lcom/github/ajalt/mordant/rendering/Theme;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;ILjava/lang/Boolean;Lcom/github/ajalt/mordant/terminal/TerminalInterface;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lcom/github/ajalt/mordant/rendering/AnsiLevel;Lcom/github/ajalt/mordant/rendering/Theme;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;ILjava/lang/Boolean;Lcom/github/ajalt/mordant/terminal/TerminalInterface;)V + public synthetic fun (Lcom/github/ajalt/mordant/rendering/AnsiLevel;Lcom/github/ajalt/mordant/rendering/Theme;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;ILjava/lang/Boolean;Lcom/github/ajalt/mordant/terminal/TerminalInterface;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun danger (Ljava/lang/Object;Lcom/github/ajalt/mordant/rendering/Whitespace;Lcom/github/ajalt/mordant/rendering/TextAlign;Lcom/github/ajalt/mordant/rendering/OverflowWrap;Ljava/lang/Integer;Z)V public static synthetic fun danger$default (Lcom/github/ajalt/mordant/terminal/Terminal;Ljava/lang/Object;Lcom/github/ajalt/mordant/rendering/Whitespace;Lcom/github/ajalt/mordant/rendering/TextAlign;Lcom/github/ajalt/mordant/rendering/OverflowWrap;Ljava/lang/Integer;ZILjava/lang/Object;)V public final fun getColors ()Lcom/github/ajalt/mordant/terminal/TerminalColors; diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/Terminal.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/Terminal.kt index 82d83fd1c..02ac9a6ff 100644 --- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/Terminal.kt +++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/Terminal.kt @@ -18,19 +18,27 @@ import com.github.ajalt.mordant.widgets.Text * `interactive`. */ class Terminal private constructor( + /** The theme to use when rendering widgets */ val theme: Theme, + /** The number of spaces to use when printing tab characters. */ val tabWidth: Int, + /** The interface to use to interact with the system terminal. */ val terminalInterface: TerminalInterface, private val forceWidth: Int?, private val forceHeight: Int?, + private val nonInteractiveWidth: Int?, + private val nonInteractiveHeight: Int?, /** The terminal capabilities that were detected or set in the constructor. */ val info: TerminalInfo, ) { + /** * @param ansiLevel The level of color support to use, or `null` to detect the level of the current terminal * @param theme The theme to use for widgets and styles like [success] * @param width The width to render widget and wrap text, or `null` to detect the current width. * @param height The height of the terminal to use when rendering widgets, or `null` to detect the current width. + * @param nonInteractiveWidth The width to use when the terminal is not interactive, or `null` to use the default of 79 columns. + * @param nonInteractiveHeight The height to use when the terminal is not interactive, or `null` to use the default of 24 rows. * @param hyperlinks whether to render hyperlinks using ANSI codes, or `null` to detect the capability * @param tabWidth The number of spaces to use for `\t` characters * @param interactive Set to true to always use color codes, even if stdout is redirected to a @@ -43,6 +51,8 @@ class Terminal private constructor( theme: Theme = Theme.Default, width: Int? = null, height: Int? = null, + nonInteractiveWidth: Int? = null, + nonInteractiveHeight: Int? = null, hyperlinks: Boolean? = null, tabWidth: Int = 8, interactive: Boolean? = null, @@ -53,6 +63,8 @@ class Terminal private constructor( terminalInterface = terminalInterface, forceWidth = width, forceHeight = height, + nonInteractiveWidth = nonInteractiveWidth, + nonInteractiveHeight = nonInteractiveHeight, info = terminalInterface.info( ansiLevel = ansiLevel, hyperlinks = hyperlinks, @@ -65,7 +77,15 @@ class Terminal private constructor( MppAtomicRef(emptyList()) private val atomicSize: MppAtomicRef = - MppAtomicRef(terminalInterface.detectSize(forceWidth, forceHeight)) + MppAtomicRef( + terminalInterface.detectSize( + info, + forceWidth, + forceHeight, + nonInteractiveWidth, + nonInteractiveHeight + ) + ) /** @@ -81,7 +101,15 @@ class Terminal private constructor( * This is called automatically whenever you print to the terminal. */ fun updateSize(): Size { - return atomicSize.update { terminalInterface.detectSize(forceWidth, forceHeight) }.second + return atomicSize.update { + terminalInterface.detectSize( + info, + forceWidth, + forceHeight, + nonInteractiveWidth, + nonInteractiveHeight + ) + }.second } /** diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalDetection.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalDetection.kt index 1ad4a69ba..186d69d91 100644 --- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalDetection.kt +++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/terminal/TerminalDetection.kt @@ -200,11 +200,23 @@ object TerminalDetection { } } -internal fun TerminalInterface.detectSize(width: Int?, height: Int?): Size { +private const val DEFAULT_WIDTH = 79 +private const val DEFAULT_HEIGHT = 24 +internal fun TerminalInterface.detectSize( + info: TerminalInfo, + width: Int?, + height: Int?, + nonInteractiveWidth: Int?, + nonInteractiveHeight: Int?, +): Size { if (width != null && height != null) return Size(width, height) + if (!info.outputInteractive) return Size( + nonInteractiveWidth ?: width ?: DEFAULT_WIDTH, + nonInteractiveHeight ?: height ?: DEFAULT_HEIGHT + ) val detected = getTerminalSize() ?: Size( - width = (getEnv("COLUMNS")?.toIntOrNull() ?: 79), - height = (getEnv("LINES")?.toIntOrNull() ?: 24) + width = (getEnv("COLUMNS")?.toIntOrNull() ?: DEFAULT_WIDTH), + height = (getEnv("LINES")?.toIntOrNull() ?: DEFAULT_HEIGHT) ) return Size(width = width ?: detected.width, height = height ?: detected.height) } diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalTest.kt index 6723f5d7d..84a21cd9e 100644 --- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalTest.kt +++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/terminal/TerminalTest.kt @@ -3,6 +3,8 @@ package com.github.ajalt.mordant.terminal import com.github.ajalt.mordant.rendering.TextAlign import com.github.ajalt.mordant.rendering.TextColors.cyan import com.github.ajalt.mordant.rendering.Whitespace +import io.kotest.data.blocking.forAll +import io.kotest.data.row import io.kotest.matchers.shouldBe import kotlin.js.JsName import kotlin.test.Test @@ -84,4 +86,23 @@ class TerminalTest { |${cyan(" wrap")} """.trimMargin() } + + @[Test JsName("width_override")] + fun `width override`() = forAll( + row(1, 2, true, 1), + row(1, 2, false, 2), + row(null, 2, true, 3), + row(null, 2, false, 2), + row(1, null, true, 1), + row(1, null, false, 1), + row(null, null, true, 3), + ) { width, niWidth, interactive, expected -> + val vt = TerminalRecorder(width = 3, outputInteractive = interactive) + val t = Terminal( + terminalInterface = vt, + width = width, + nonInteractiveWidth = niWidth + ) + t.size.width shouldBe expected + } } diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt index 6e9a1323d..420865985 100644 --- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt +++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt @@ -19,7 +19,14 @@ abstract class RenderingTest( theme: Theme = Theme.Default, transformActual: (String) -> String = { it }, ) { - val t = Terminal(AnsiLevel.TRUECOLOR, theme, width, height, hyperlinks, tabWidth) + val t = Terminal( + ansiLevel = AnsiLevel.TRUECOLOR, + theme = theme, + width = width, + height = height, + hyperlinks = hyperlinks, + tabWidth = tabWidth + ) val actual = transformActual(t.render(widget)) actual.shouldMatchRender(expected, trimMargin) }