diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f17004..0797165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v5 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' @@ -28,4 +28,4 @@ jobs: # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@df268dd1684fb95352aa463056e3cf41f9acb94b + uses: advanced-security/maven-dependency-submission-action@b275d12641ac2d2108b2cbb7598b154ad2f2cee8 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 8dea6c2..d58dfb7 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,19 @@ -wrapperVersion=3.3.4 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/README.md b/README.md index ed30078..39041aa 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,10 @@ # Twinkle -Twinkle is a Java utility library for creating text-based user interfaces. +Twinkle is a Java library for creating advanced text-based user interfaces. This is a very early proof of concept, nothing to see here (yet) -## Components - -- `twinkle-ansi` - A library for working with ANSI escape codes, providing utilities for styling and formatting text in the terminal. -- `twinkle-text` - A library for managing, manipulating and rendering text. - ## Building To build the project, run the following command: @@ -20,4 +15,19 @@ To build the project, run the following command: ## Running -Nothing to see here yet... +To see a couple of very early demos, run the following commands: + +```bash +java -cp twinkle-chart/target/twinkle-chart-1.0-SNAPSHOT.jar:twinkle-core/target/twinkle-core-1.0-SNAPSHOT.jar:twinkle-ansi/target/twinkle-ansi-1.0-SNAPSHOT.jar:twinkle-chart/target/test-classes examples.BarDemo +java -cp twinkle-chart/target/twinkle-chart-1.0-SNAPSHOT.jar:twinkle-core/target/twinkle-core-1.0-SNAPSHOT.jar:twinkle-ansi/target/twinkle-ansi-1.0-SNAPSHOT.jar:twinkle-chart/target/test-classes examples.MathPlotDemo +``` + +An easier way to run the demos is using [JBang](https://www.jbang.dev/): + +```bash +./mvnw install -DskipTests +jbang run examples/BarDemo.java +jbang run examples/MathPlotDemo.java +``` + +These demos only show Twinkle's Ansi output capabilities. There is no interactivity being shown. diff --git a/app.yml b/app.yml index 0b1ccca..c9a8e4e 100644 --- a/app.yml +++ b/app.yml @@ -1,5 +1,5 @@ name: twinkle -description: Twinkle is a Java library for creating text-based user interfaces. +description: Twinkle is a Java library for creating advanced text-based user interfaces. authors: - Tako Schotanus (tako@codejive.org) links: @@ -12,3 +12,5 @@ actions: clean: ./mvnw clean build: ./mvnw spotless:apply package -DskipTests test: ./mvnw test + runbar: java -cp twinkle-chart/target/twinkle-chart-1.0-SNAPSHOT.jar:twinkle-core/target/twinkle-core-1.0-SNAPSHOT.jar:twinkle-ansi/target/twinkle-ansi-1.0-SNAPSHOT.jar:twinkle-chart/target/test-classes examples.BarDemo + runplot: java -cp twinkle-chart/target/twinkle-chart-1.0-SNAPSHOT.jar:twinkle-core/target/twinkle-core-1.0-SNAPSHOT.jar:twinkle-ansi/target/twinkle-ansi-1.0-SNAPSHOT.jar:twinkle-chart/target/test-classes examples.PlotDemo diff --git a/jbang-catalog.json b/jbang-catalog.json index 215dbdc..8b74789 100644 --- a/jbang-catalog.json +++ b/jbang-catalog.json @@ -1,4 +1,16 @@ { "aliases": { + "BarDemo": { + "script-ref": "twinkle-chart\\src\\test\\java\\examples\\BarDemo.java", + "dependencies": [ + "org.codejive.twinkle:twinkle-chart:1.0-SNAPSHOT" + ] + }, + "MathPlotDemo": { + "script-ref": "twinkle-chart\\src\\test\\java\\examples\\MathPlotDemo.java", + "dependencies": [ + "org.codejive.twinkle:twinkle-chart:1.0-SNAPSHOT" + ] + } } } \ No newline at end of file diff --git a/mvnw b/mvnw index bd8896b..19529dd 100755 --- a/mvnw +++ b/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 +# Apache Maven Wrapper startup batch script, version 3.3.2 # # Optional ENV vars # ----------------- @@ -105,17 +105,14 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -133,7 +130,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -230,7 +227,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -255,41 +252,8 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 92450f9..249bdf3 100755 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,30 +73,16 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" } - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -148,33 +134,7 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/pom.xml b/pom.xml index f1dd51a..d42c262 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ UTF-8 - 2.46.1 + 2.44.4 1.22.0 1.0.0 6.0.1 @@ -19,8 +19,10 @@ + twinkle-core twinkle-ansi - twinkle-text + twinkle-chart + @@ -71,7 +73,7 @@ ${google-java-format.version} - + @@ -89,9 +91,8 @@ 8 8 - 21 - 21 - + 11 + 11 diff --git a/twinkle-ansi/pom.xml b/twinkle-ansi/pom.xml index a386ff5..cef7341 100644 --- a/twinkle-ansi/pom.xml +++ b/twinkle-ansi/pom.xml @@ -14,7 +14,7 @@ twinkle-ansi jar - Generic ANSI support + Generic ANSI support classes diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java index 4fc5b38..cf16542 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Ansi.java @@ -1,268 +1,131 @@ package org.codejive.twinkle.ansi; -import static org.codejive.twinkle.ansi.Constants.*; - -import java.io.IOException; - public class Ansi { - public static final String STYLE_RESET = styles(RESET); // Reset all attributes + public static final char ESC = 27; + + public static final String CSI = ESC + "["; // Control Sequence Introducer + + // Style codes + public static final int RESET = 0; // Reset all attributes + public static final int BOLD = 1; // Bold text + public static final int FAINT = 2; // Faint/dim text + public static final int ITALICIZED = 3; // Italic text + public static final int UNDERLINED = 4; // Underlined text + public static final int BLINK = 5; // Blinking text + public static final int INVERSE = 7; // Reversed foreground + public static final int INVISIBLE = 8; // Invisible text + public static final int CROSSEDOUT = 9; // Strike-through + public static final int DOUBLEUNDERLINE = 21; // Double underline + public static final int NORMAL = 22; // Normal intensity + public static final int NOTITALICIZED = 23; // Not italic + public static final int NOTUNDERLINED = 24; // Not underlined + public static final int STEADY = 25; // Not blinking + public static final int POSITIVE = 27; // Positive image + public static final int VISIBLE = 28; // Visible text + public static final int NOTCROSSEDOUT = 29; // Not strike-through + public static final int DEFAULT_FOREGROUND = 39; + public static final int DEFAULT_BACKGROUND = 49; + + public static final String STYLE_RESET = style(RESET); // Reset all attributes public static final String STYLE_DEFAULT_FOREGROUND = - styles(DEFAULT_FOREGROUND); // Reset foreground color to default + style(DEFAULT_FOREGROUND); // Reset all attributes public static final String STYLE_DEFAULT_BACKGROUND = - styles(DEFAULT_BACKGROUND); // Reset background color + style(DEFAULT_BACKGROUND); // Reset all attributes + + public static final int BLACK = 0; + public static final int RED = 1; + public static final int GREEN = 2; + public static final int YELLOW = 3; + public static final int BLUE = 4; + public static final int MAGENTA = 5; + public static final int CYAN = 6; + public static final int WHITE = 7; + + public static final int FOREGROUND_BASE = 30; + public static final int FOREGROUND_DARK_BASE = 60; + public static final int FOREGROUND_BRIGHT_BASE = 90; + public static final int BACKGROUND_BASE = 40; + public static final int BACKGROUND_DARK_BASE = 70; + public static final int BACKGROUND_BRIGHT_BASE = 100; + + public static final int FOREGROUND_COLORS = 38; + public static final int BACKGROUND_COLORS = 48; + public static final int COLORS_RGB = 2; + public static final int COLORS_INDEXED = 5; + + // OSC 8 hyperlinks (BEL-terminated) + public static String osc8Open(String url) { + return "\u001B]8;;" + url + "\u0007"; + } + + public static String osc8Close() { + return "\u001B]8;;\u0007"; + } - /** - * Returns the ANSI escape sequence for the given styles. The styles can be any combination of - * the style constants defined in the Constants class, such as BOLD, UNDERLINED, or the output - * of the color functions like foregroundArg(). The output is a string that can be used in the - * console to apply the specified styles to the text that follows. - * - * @param styles the style codes to apply - * @return the ANSI escape sequence for the given styles - */ - public static String styles(Object... styles) { + public static String style(Object... styles) { if (styles == null || styles.length == 0) { return ""; } - return styles(new StringBuilder(), styles).toString(); + return style(new StringBuilder(), styles).toString(); } - /** - * Appends the ANSI escape sequence for the given styles to the provided Appendable. The styles - * can be any combination of the style constants defined in the Constants class, such as BOLD, - * UNDERLINED, or the output of the color functions like foregroundArg(). The output will be - * passed to the provided Appendable. - * - * @param appendable the Appendable to which the ANSI escape sequence will be appended - * @param styles the style codes to apply - * @return the provided Appendable with the ANSI escape sequence appended - */ - public static Appendable styles(Appendable appendable, Object... styles) { + public static StringBuilder style(StringBuilder sb, Object... styles) { if (styles == null || styles.length == 0) { - return appendable; + return sb; } - try { - appendable.append(CSI); - for (int i = 0; i < styles.length; i++) { - appendable.append(styles[i].toString()); - if (i < styles.length - 1) { - appendable.append(";"); - } + sb.append(CSI); + for (int i = 0; i < styles.length; i++) { + sb.append(styles[i]); + if (i < styles.length - 1) { + sb.append(";"); } - appendable.append("m"); - } catch (IOException e) { - throw new RuntimeException(e); } - return appendable; + sb.append("m"); + return sb; } - /** - * Returns the ANSI code for the given basic color. The index should be between 0-7. The output - * is a string that can be used in the styles() method to create the final ANSI escape sequence. - * - * @param index the color index (0-7) - * @return the ANSI code for the given basic color - */ - public static String foregroundArg(int index) { + public static String foreground(int index) { return String.valueOf(FOREGROUND_BASE + index); } - /** - * Returns the ANSI code for the given dark color. The index should be between 0-7. The output - * is a string that can be used in the styles() method to create the final ANSI escape sequence. - * - * @param index the color index (0-7) - * @return the ANSI code for the given dark color - */ - public static String foregroundDarkArg(int index) { + public static String foregroundDark(int index) { return String.valueOf(FOREGROUND_DARK_BASE + index); } - /** - * Returns the ANSI code for the given bright color. The index should be between 0-7. The output - * is a string that can be used in the styles() method to create the final ANSI escape sequence. - * - * @param index the color index (0-7) - * @return the ANSI code for the given bright color - */ - public static String foregroundBrightArg(int index) { + public static String foregroundBright(int index) { return String.valueOf(FOREGROUND_BRIGHT_BASE + index); } - /** - * Returns the ANSI code for the given basic background color. The index should be between 0-7. - * The output is a string that can be used in the styles() method to create the final ANSI - * escape sequence. - * - * @param index the color index (0-7) - * @return the ANSI code for the given basic background color - */ - public static String backgroundArg(int index) { + public static String background(int index) { return String.valueOf(BACKGROUND_BASE + index); } - /** - * Returns the ANSI code for the given dark background color. The index should be between 0-7. - * The output is a string that can be used in the styles() method to create the final ANSI - * escape sequence. - * - * @param index the color index (0-7) - * @return the ANSI code for the given dark background color - */ - public static String backgroundDarkArg(int index) { + public static String backgroundDark(int index) { return String.valueOf(BACKGROUND_DARK_BASE + index); } - /** - * Returns the ANSI code for the given bright background color. The index should be between 0-7. - * The output is a string that can be used in the styles() method to create the final ANSI - * escape sequence. - * - * @param index the color index (0-7) - * @return the ANSI code for the given bright background color - */ - public static String backgroundBrightArg(int index) { + public static String backgroundBright(int index) { return String.valueOf(BACKGROUND_BRIGHT_BASE + index); } - /** - * Returns the ANSI code for the given indexed foreground color. The index should be between - * 0-255. The indexes 0-7 are the standard colors, 8-15 are the bright versions of the standard - * colors, 16-231 are a 6x6x6 color cube, and 232-255 are a grayscale ramp. The output is a - * string that can be used in the styles() method to create the final ANSI escape sequence. - * - * @param index the color index (0-255) - * @return the ANSI code for the given indexed foreground color - */ - public static String foregroundIndexedArg(int index) { + public static String foregroundIndexed(int index) { + // Standard SGR: 38;5; return FOREGROUND_COLORS + ";" + COLORS_INDEXED + ";" + index; } - /** - * Returns the ANSI code for the given indexed background color. The index should be between - * 0-255. The indexes 0-7 are the standard colors, 8-15 are the bright versions of the standard - * colors, 16-231 are a 6x6x6 color cube, and 232-255 are a grayscale ramp. The output is a - * string that can be used in the styles() method to create the final ANSI escape sequence. - * - * @param index the color index (0-255) - * @return the ANSI code for the given indexed background color - */ - public static String backgroundIndexedArg(int index) { - return BACKGROUND_COLORS + ";" + COLORS_INDEXED + ";" + index; - } - - /** - * Returns the ANSI code for the given RGB foreground color. The r, g, and b values should be - * between 0-255. The output is a string that can be used in the styles() method to create the - * final ANSI escape sequence. - * - * @param r the red component (0-255) - * @param g the green component (0-255) - * @param b the blue component (0-255) - * @return the ANSI code for the given RGB foreground color - */ - public static String foregroundRgbArg(int r, int g, int b) { + public static String foregroundRgb(int r, int g, int b) { + // Standard SGR truecolor: 38;2;;; return FOREGROUND_COLORS + ";" + COLORS_RGB + ";" + r + ";" + g + ";" + b; } - /** - * Returns the ANSI code for the given RGB background color. The r, g, and b values should be - * between 0-255. The output is a string that can be used in the styles() method to create the - * final ANSI escape sequence. - * - * @param r the red component (0-255) - * @param g the green component (0-255) - * @param b the blue component (0-255) - * @return the ANSI code for the given RGB background color - */ - public static String backgroundRgbArg(int r, int g, int b) { - return BACKGROUND_COLORS + ";" + COLORS_RGB + ";" + r + ";" + g + ";" + b; - } - - /** - * Returns the ANSI escape sequence for moving the cursor in the specified direction by 1. The - * direction is determined by the command parameter, which can be one of the cursor movement - * commands defined in the Constants class, such as CURSOR_UP, CURSOR_DOWN, etc. - * - * @param command the cursor movement command (e.g. CURSOR_UP, CURSOR_DOWN, etc.) - * @return the ANSI escape sequence for moving the cursor - */ - public static String cursorMove(char command) { - return cursorMove(command, 1); - } - - /** - * Returns the ANSI escape sequence for moving the cursor in the specified direction by the - * specified amount. The direction is determined by the command parameter, which can be one of - * the cursor movement commands defined in the Constants class, such as CURSOR_UP, CURSOR_DOWN, - * etc. The amount parameter specifies how many positions to move the cursor. - * - * @param command the cursor movement command (e.g. CURSOR_UP, CURSOR_DOWN, etc.) - * @param amount the number of positions to move the cursor - * @return the ANSI escape sequence for moving the cursor - */ - public static String cursorMove(char command, int amount) { - return CSI + amount + command; - } - - public static String cursorPos(int row, int col) { - return CSI + row + ";" + col + CURSOR_POSITION; - } - - public static String cursorHome() { - return CSI + CURSOR_POSITION; - } - - public static String cursorToColumn(int col) { - return CSI + col + CURSOR_COLUMN; - } - - public static String cursorUp(int amount) { - return cursorMove(CURSOR_UP, amount); - } - - public static String cursorDown(int amount) { - return cursorMove(CURSOR_DOWN, amount); - } - - public static String cursorForward(int amount) { - return cursorMove(CURSOR_FORWARD, amount); - } - - public static String cursorBackward(int amount) { - return cursorMove(CURSOR_BACKWARD, amount); - } - - public static String cursorHide() { - return CSI + CURSOR_HIDE; - } - - public static String cursorShow() { - return CSI + CURSOR_SHOW; - } - - public static String cursorSave() { - return "" + ESC + CURSOR_SAVE; - } - - public static String cursorRestore() { - return "" + ESC + CURSOR_RESTORE; - } - - public static String clearScreen() { - return CSI + SCREEN_ERASE_FULL; - } - - public static String autoWrap(boolean enabled) { - return CSI + (enabled ? LINE_WRAP_ON : LINE_WRAP_OFF); - } - - public static String linkStart(String url) { - return OSC + "8;;" + url + OSC_END; + public static String backgroundIndexed(int index) { + // Standard SGR: 48;5; + return BACKGROUND_COLORS + ";" + COLORS_INDEXED + ";" + index; } - public static String linkEnd() { - return OSC + "8;;" + OSC_END; + public static String backgroundRgb(int r, int g, int b) { + // Standard SGR truecolor: 48;2;;; + return BACKGROUND_COLORS + ";" + COLORS_RGB + ";" + r + ";" + g + ";" + b; } private Ansi() {} diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Color.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Color.java index b09e25f..be42200 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Color.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Color.java @@ -1,6 +1,6 @@ package org.codejive.twinkle.ansi; -import static org.codejive.twinkle.ansi.Constants.*; +import static org.codejive.twinkle.ansi.Ansi.*; import java.util.Objects; @@ -24,43 +24,9 @@ static RgbColor rgb(int r, int g, int b) { return RgbColor.of(r, g, b); } - /** - * Convert this color to ANSI escape code for setting the foreground color to the color - * represented by this instance. This is NOT the full CSI sequence, to get the full sequence - * pass the result to Constants.style() or use toAnsiFg(). - * - * @return Embeddable ANSI escape code string - */ - String toAnsiFgArgs(); - - /** - * Convert this color to ANSI escape code for setting the foreground color to the color - * represented by this instance. - * - * @return ANSI CSI escape code string - */ - default String toAnsiFg() { - return Ansi.styles(toAnsiFgArgs()); - } + String toAnsiFg(); - /** - * Convert this color to ANSI escape code for setting the background color to the color - * represented by this instance. This is NOT the full CSI sequence, to get the full sequence - * pass the result to Constants.style() or use toAnsiBg(). - * - * @return Embeddable ANSI escape code string - */ - String toAnsiBgArgs(); - - /** - * Convert this color to ANSI escape code for setting the background color to the color - * represented by this instance. - * - * @return ANSI CSI escape code string - */ - default String toAnsiBg() { - return Ansi.styles(toAnsiBgArgs()); - } + String toAnsiBg(); class DefaultColor implements Color { private static final DefaultColor INSTANCE = new DefaultColor(); @@ -72,18 +38,13 @@ protected static Color instance() { } @Override - public String toAnsiFgArgs() { - return String.valueOf(DEFAULT_FOREGROUND); - } - - @Override - public String toAnsiBgArgs() { - return String.valueOf(DEFAULT_BACKGROUND); + public String toAnsiFg() { + return STYLE_DEFAULT_FOREGROUND; } @Override - public String toString() { - return "default"; + public String toAnsiBg() { + return STYLE_DEFAULT_BACKGROUND; } } @@ -100,55 +61,47 @@ public enum Intensity { bright; } - public static final BasicColor BLACK = - BasicColor.of("black", Constants.BLACK, Intensity.normal); - public static final BasicColor RED = BasicColor.of("red", Constants.RED, Intensity.normal); - public static final BasicColor GREEN = - BasicColor.of("green", Constants.GREEN, Intensity.normal); + public static final BasicColor BLACK = BasicColor.of("black", Ansi.BLACK, Intensity.normal); + public static final BasicColor RED = BasicColor.of("red", Ansi.RED, Intensity.normal); + public static final BasicColor GREEN = BasicColor.of("green", Ansi.GREEN, Intensity.normal); public static final BasicColor YELLOW = - BasicColor.of("yellow", Constants.YELLOW, Intensity.normal); - public static final BasicColor BLUE = - BasicColor.of("blue", Constants.BLUE, Intensity.normal); + BasicColor.of("yellow", Ansi.YELLOW, Intensity.normal); + public static final BasicColor BLUE = BasicColor.of("blue", Ansi.BLUE, Intensity.normal); public static final BasicColor MAGENTA = - BasicColor.of("magenta", Constants.MAGENTA, Intensity.normal); - public static final BasicColor CYAN = - BasicColor.of("cyan", Constants.CYAN, Intensity.normal); - public static final BasicColor WHITE = - BasicColor.of("white", Constants.WHITE, Intensity.normal); + BasicColor.of("magenta", Ansi.MAGENTA, Intensity.normal); + public static final BasicColor CYAN = BasicColor.of("cyan", Ansi.CYAN, Intensity.normal); + public static final BasicColor WHITE = BasicColor.of("white", Ansi.WHITE, Intensity.normal); public static final BasicColor DARK_BLACK = - BasicColor.of("black", Constants.BLACK, Intensity.dark); - public static final BasicColor DARK_RED = - BasicColor.of("red", Constants.RED, Intensity.dark); + BasicColor.of("black", Ansi.BLACK, Intensity.dark); + public static final BasicColor DARK_RED = BasicColor.of("red", Ansi.RED, Intensity.dark); public static final BasicColor DARK_GREEN = - BasicColor.of("green", Constants.GREEN, Intensity.dark); + BasicColor.of("green", Ansi.GREEN, Intensity.dark); public static final BasicColor DARK_YELLOW = - BasicColor.of("yellow", Constants.YELLOW, Intensity.dark); - public static final BasicColor DARK_BLUE = - BasicColor.of("blue", Constants.BLUE, Intensity.dark); + BasicColor.of("yellow", Ansi.YELLOW, Intensity.dark); + public static final BasicColor DARK_BLUE = BasicColor.of("blue", Ansi.BLUE, Intensity.dark); public static final BasicColor DARK_MAGENTA = - BasicColor.of("magenta", Constants.MAGENTA, Intensity.dark); - public static final BasicColor DARK_CYAN = - BasicColor.of("cyan", Constants.CYAN, Intensity.dark); + BasicColor.of("magenta", Ansi.MAGENTA, Intensity.dark); + public static final BasicColor DARK_CYAN = BasicColor.of("cyan", Ansi.CYAN, Intensity.dark); public static final BasicColor DARK_WHITE = - BasicColor.of("white", Constants.WHITE, Intensity.dark); + BasicColor.of("white", Ansi.WHITE, Intensity.dark); public static final BasicColor BRIGHT_BLACK = - BasicColor.of("black", Constants.BLACK, Intensity.bright); + BasicColor.of("black", Ansi.BLACK, Intensity.bright); public static final BasicColor BRIGHT_RED = - BasicColor.of("red", Constants.RED, Intensity.bright); + BasicColor.of("red", Ansi.RED, Intensity.bright); public static final BasicColor BRIGHT_GREEN = - BasicColor.of("green", Constants.GREEN, Intensity.bright); + BasicColor.of("green", Ansi.GREEN, Intensity.bright); public static final BasicColor BRIGHT_YELLOW = - BasicColor.of("yellow", Constants.YELLOW, Intensity.bright); + BasicColor.of("yellow", Ansi.YELLOW, Intensity.bright); public static final BasicColor BRIGHT_BLUE = - BasicColor.of("blue", Constants.BLUE, Intensity.bright); + BasicColor.of("blue", Ansi.BLUE, Intensity.bright); public static final BasicColor BRIGHT_MAGENTA = - BasicColor.of("magenta", Constants.MAGENTA, Intensity.bright); + BasicColor.of("magenta", Ansi.MAGENTA, Intensity.bright); public static final BasicColor BRIGHT_CYAN = - BasicColor.of("cyan", Constants.CYAN, Intensity.bright); + BasicColor.of("cyan", Ansi.CYAN, Intensity.bright); public static final BasicColor BRIGHT_WHITE = - BasicColor.of("white", Constants.WHITE, Intensity.bright); + BasicColor.of("white", Ansi.WHITE, Intensity.bright); private static final BasicColor[] normalColors = { BLACK, RED, GREEN, YELLOW, @@ -228,22 +181,22 @@ public BasicColor bright() { } } - public String toAnsiFgArgs() { + public String toAnsiFg() { return fgAnsi; } - public String toAnsiBgArgs() { + public String toAnsiBg() { return bgAnsi; } private static String fgAnsi(int index, Intensity intensity) { switch (intensity) { case normal: - return Ansi.foregroundArg(index); + return Ansi.style(Ansi.foreground(index)); case dark: - return Ansi.foregroundDarkArg(index); + return Ansi.style(Ansi.foregroundDark(index)); case bright: - return Ansi.foregroundBrightArg(index); + return Ansi.style(Ansi.foregroundBright(index)); default: throw new IllegalArgumentException("Unknown mode: " + intensity); } @@ -252,11 +205,11 @@ private static String fgAnsi(int index, Intensity intensity) { private static String bgAnsi(int index, Intensity intensity) { switch (intensity) { case normal: - return Ansi.backgroundArg(index); + return Ansi.style(Ansi.background(index)); case dark: - return Ansi.backgroundDarkArg(index); + return Ansi.style(Ansi.backgroundDark(index)); case bright: - return Ansi.backgroundBrightArg(index); + return Ansi.style(Ansi.backgroundBright(index)); default: throw new IllegalArgumentException("Unknown mode: " + intensity); } @@ -313,21 +266,21 @@ public int index() { } @Override - public String toAnsiFgArgs() { + public String toAnsiFg() { return fgAnsi; } @Override - public String toAnsiBgArgs() { + public String toAnsiBg() { return bgAnsi; } private static String fgAnsi(int index) { - return Ansi.foregroundIndexedArg(index); + return Ansi.style(Ansi.foregroundIndexed(index)); } private static String bgAnsi(int index) { - return Ansi.backgroundIndexedArg(index); + return Ansi.style(Ansi.backgroundIndexed(index)); } @Override @@ -382,21 +335,21 @@ public int b() { } @Override - public String toAnsiFgArgs() { + public String toAnsiFg() { return fgAnsi; } @Override - public String toAnsiBgArgs() { + public String toAnsiBg() { return bgAnsi; } private static String fgAnsi(int r, int g, int b) { - return Ansi.foregroundRgbArg(r, g, b); + return Ansi.style(Ansi.foregroundRgb(r, g, b)); } private static String bgAnsi(int r, int g, int b) { - return Ansi.backgroundRgbArg(r, g, b); + return Ansi.style(Ansi.backgroundRgb(r, g, b)); } @Override diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Constants.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Constants.java deleted file mode 100644 index e954dc8..0000000 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Constants.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.codejive.twinkle.ansi; - -public class Constants { - public static final char ESC = '\u001B'; - - public static final String CSI = ESC + "["; // Control Sequence Introducer - public static final String OSC = ESC + "]"; // Operating System Command - public static final String OSC_END = "\u0007"; // Bell character - - // Style codes - public static final int RESET = 0; // Reset all attributes - public static final int BOLD = 1; // Bold text - public static final int FAINT = 2; // Faint/dim text - public static final int ITALICIZED = 3; // Italic text - public static final int UNDERLINED = 4; // Underlined text - public static final int BLINK = 5; // Blinking text - public static final int INVERSE = 7; // Reversed foreground - public static final int INVISIBLE = 8; // Invisible text - public static final int CROSSEDOUT = 9; // Strike-through - public static final int DOUBLEUNDERLINE = 21; // Double underline - public static final int NORMAL = 22; // Normal intensity - public static final int NOTITALICIZED = 23; // Not italic - public static final int NOTUNDERLINED = 24; // Not underlined - public static final int STEADY = 25; // Not blinking - public static final int POSITIVE = 27; // Positive image - public static final int VISIBLE = 28; // Visible text - public static final int NOTCROSSEDOUT = 29; // Not strike-through - public static final int DEFAULT_FOREGROUND = 39; // Default foreground color - public static final int DEFAULT_BACKGROUND = 49; // Default background color - - public static final int BLACK = 0; - public static final int RED = 1; - public static final int GREEN = 2; - public static final int YELLOW = 3; - public static final int BLUE = 4; - public static final int MAGENTA = 5; - public static final int CYAN = 6; - public static final int WHITE = 7; - - public static final int FOREGROUND_BASE = 30; - public static final int FOREGROUND_DARK_BASE = 60; - public static final int FOREGROUND_BRIGHT_BASE = 90; - public static final int BACKGROUND_BASE = 40; - public static final int BACKGROUND_DARK_BASE = 70; - public static final int BACKGROUND_BRIGHT_BASE = 100; - - public static final int FOREGROUND_COLORS = 38; - public static final int BACKGROUND_COLORS = 48; - public static final int COLORS_RGB = 2; - public static final int COLORS_INDEXED = 5; - - public static final char CURSOR_UP = 'A'; - public static final char CURSOR_DOWN = 'B'; - public static final char CURSOR_FORWARD = 'C'; - public static final char CURSOR_BACKWARD = 'D'; - public static final char CURSOR_NEXT_LINE = 'E'; - public static final char CURSOR_PREV_LINE = 'F'; - public static final char CURSOR_COLUMN = 'G'; - public static final char CURSOR_POSITION = 'H'; - public static final char CURSOR_POSITION_ALT = 'f'; - public static final char CURSOR_SAVE = '7'; // Note: ESC+7, not CSI+7 ! - public static final char CURSOR_RESTORE = '8'; // Note: ESC+8, not CSI+8 ! - public static final char CURSOR_UP_WITH_SCROLL = 'M'; // Note: ESC+M, not CSI+M ! - - public static final String CURSOR_HIDE = "?25l"; - public static final String CURSOR_SHOW = "?25h"; - - public static final String SCREEN_ERASE = "J"; // Same as SCREEN_ERASE_END - public static final String SCREEN_ERASE_END = "0J"; - public static final String SCREEN_ERASE_START = "1J"; - public static final String SCREEN_ERASE_FULL = "2J"; - public static final String SCREEN_ERASE_SAVED_LINES = "3J"; - - public static final String SCREEN_SAVE = "?1049h"; - public static final String SCREEN_SAVE_ALT = "?47h"; - public static final String SCREEN_RESTORE = "?1049l"; - public static final String SCREEN_RESTORE_ALT = "?47l"; - - public static final String LINE_ERASE = "K"; // Same as LINE_ERASE_END - public static final String LINE_ERASE_END = "0K"; - public static final String LINE_ERASE_START = "1K"; - public static final String LINE_ERASE_FULL = "2K"; - - public static final String LINE_WRAP_ON = "=7h"; - public static final String LINE_WRAP_OFF = "=7l"; -} diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java index 03b4632..7ca0b72 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java @@ -2,12 +2,11 @@ import java.util.ArrayList; import java.util.List; -import org.codejive.twinkle.ansi.util.Printable; + import org.jspecify.annotations.NonNull; -public class Style implements Printable { +public class Style { private final long state; - private final long mask; private static final long IDX_BOLD = 0; private static final long IDX_FAINT = 1; @@ -19,9 +18,7 @@ public class Style implements Printable { private static final long IDX_CROSSEDOUT = 7; // private static final long DOUBLEUNDERLINE = 8; - public static final long F_UNKNOWN = -1L; public static final long F_UNSTYLED = 0L; - public static final long F_BOLD = 1 << IDX_BOLD; public static final long F_FAINT = 1 << IDX_FAINT; public static final long F_ITALIC = 1 << IDX_ITALICIZED; @@ -31,6 +28,16 @@ public class Style implements Printable { public static final long F_HIDDEN = 1 << IDX_INVISIBLE; public static final long F_STRIKETHROUGH = 1 << IDX_CROSSEDOUT; + public static final Style UNSTYLED = new Style(0); + public static final Style BOLD = UNSTYLED.bold(); + public static final Style FAINT = UNSTYLED.faint(); + public static final Style ITALIC = UNSTYLED.italic(); + public static final Style UNDERLINED = UNSTYLED.underlined(); + public static final Style BLINK = UNSTYLED.blink(); + public static final Style INVERSE = UNSTYLED.inverse(); + public static final Style HIDDEN = UNSTYLED.hidden(); + public static final Style STRIKETHROUGH = UNSTYLED.strikethrough(); + public static @NonNull Style ofFgColor(@NonNull Color color) { return UNSTYLED.fgColor(color); } @@ -90,28 +97,6 @@ public class Style implements Printable { private static final long MASK_COLOR_BASIC_INTENSITY = 0x03L; private static final long MASK_COLOR_BASIC_INDEX = 0x07L; private static final long MASK_COLOR_PART = 0xffL; - private static final long MASK_STYLES = - F_BOLD - | F_FAINT - | F_ITALIC - | F_UNDERLINED - | F_BLINK - | F_INVERSE - | F_HIDDEN - | F_STRIKETHROUGH; - private static final long MASK_ALL = MASK_FG_COLOR | MASK_BG_COLOR | MASK_STYLES; - - public static final Style UNKNOWN = new Style(F_UNKNOWN, 0); - public static final Style UNSTYLED = new Style(F_UNSTYLED, 0); - public static final Style DEFAULT = new Style(F_UNSTYLED, MASK_ALL); - public static final Style BOLD = UNSTYLED.bold(); - public static final Style FAINT = UNSTYLED.faint(); - public static final Style ITALIC = UNSTYLED.italic(); - public static final Style UNDERLINED = UNSTYLED.underlined(); - public static final Style BLINK = UNSTYLED.blink(); - public static final Style INVERSE = UNSTYLED.inverse(); - public static final Style HIDDEN = UNSTYLED.hidden(); - public static final Style STRIKETHROUGH = UNSTYLED.strikethrough(); private static final long CM_INDEXED = 0; private static final long CM_RGB = 1; @@ -121,287 +106,141 @@ public class Style implements Printable { // Not really an intensity, but a flag to indicate default color, // but we're (ab)using the intensity bits to store it - private static final int INTENSITY_DEFAULT = 0; + private static final long INTENSITY_DEFAULT = 0; - private static final int INTENSITY_NORMAL = 1; - private static final int INTENSITY_DARK = 2; - private static final int INTENSITY_BRIGHT = 3; + private static final long INTENSITY_NORMAL = 1; + private static final long INTENSITY_DARK = 2; + private static final long INTENSITY_BRIGHT = 3; public static @NonNull Style of(long state) { - if (state == F_UNKNOWN) { - return UNKNOWN; - } - if (state == F_UNSTYLED) { - return DEFAULT; - } - return new Style(state, MASK_ALL); - } - - public static @NonNull Style of(long state, long mask) { - if (state == F_UNKNOWN) { - return UNKNOWN; - } - if (state == F_UNSTYLED) { - if ((mask & MASK_ALL) == 0) { - return UNSTYLED; - } else if ((mask & MASK_ALL) == MASK_ALL) { - return DEFAULT; - } + if (state == 0) { + return UNSTYLED; } - return new Style(state, mask); + return new Style(state); } - private Style(long state, long mask) { + private Style(long state) { this.state = state; - this.mask = mask; } public long state() { return state; } - public long mask() { - return mask; - } - - /** - * Combines this style with another style, giving precedence to the other style's values - * wherever it has an effect. - * - * @param other The other style to combine with. - * @return A new Style instance representing the combined style. - */ - public Style and(@NonNull Style other) { - if (this.equals(UNKNOWN)) { - return other; - } - if (other.equals(UNKNOWN)) { - return this; - } - - long newState = (this.state & ~other.mask) | (other.state & other.mask); - long newMask = this.mask | other.mask; - return of(newState, newMask); - } - - /** - * Computes the difference between this style and another style, producing a new style that - * represents the changes needed to transform this style into the other style. - * - * @param other The other style to compare with. - * @return A new Style instance representing the difference. - */ - public Style diff(@NonNull Style other) { - if (this.equals(UNKNOWN)) { - return other; - } - if (other.equals(UNKNOWN)) { - return this; - } - - long newMask = this.mask | other.mask; - long newState = other.state & newMask; - return of(newState, newMask); - } - - /** - * Returns a new style that represents the style that would result from applying the other style - * on top of this one. Styles that are changed to their unset or default values in the resulting - * style will be marked as unaffected. - * - * @param other The other style to apply. - * @return A new Style instance representing the resulting style. - */ - public Style apply(@NonNull Style other) { - if (this.equals(UNKNOWN)) { - return other; - } - if (other.equals(UNKNOWN)) { - return this; - } - - long newState = (this.state & ~other.mask) | (other.state & other.mask); - long newMask = this.mask | other.mask; - - // now mark unset styles as unaffected - long unaffectedMask = ~(other.mask & ~other.state & MASK_STYLES); - newMask &= unaffectedMask; - - if (other.affectsFgColor() && other.fgColor().equals(Color.DEFAULT)) { - newState &= ~MASK_FG_COLOR; - newMask &= ~MASK_FG_COLOR; - } - - if (other.affectsBgColor() && other.bgColor().equals(Color.DEFAULT)) { - newState &= ~MASK_BG_COLOR; - newMask &= ~MASK_BG_COLOR; - } - - return of(newState, newMask); + public @NonNull Style unstyled() { + return UNSTYLED; } - public boolean is(long flag) { - return (state & flag) != 0; + public @NonNull Style normal() { + return of(state & ~(F_BOLD | F_FAINT)); } public boolean isBold() { - return is(F_BOLD); - } - - public boolean isFaint() { - return is(F_FAINT); - } - - public boolean isItalic() { - return is(F_ITALIC); - } - - public boolean isUnderlined() { - return is(F_UNDERLINED); - } - - public boolean isBlink() { - return is(F_BLINK); - } - - public boolean isInverse() { - return is(F_INVERSE); - } - - public boolean isHidden() { - return is(F_HIDDEN); + return (state & F_BOLD) != 0; } - public boolean isStrikethrough() { - return is(F_STRIKETHROUGH); - } - - public @NonNull Color fgColor() { - long fgc = ((state & MASK_FG_COLOR) >> SHIFT_FG_COLOR); - return decodeColor(fgc); - } - - public @NonNull Color bgColor() { - long bgc = ((state & MASK_BG_COLOR) >> SHIFT_BG_COLOR); - return decodeColor(bgc); - } - - public boolean affects(long flag) { - return (mask & flag) != 0; - } - - public boolean affectsBold() { - return affects(F_BOLD); - } - - public boolean affectsFaint() { - return affects(F_FAINT); - } - - public boolean affectsItalic() { - return affects(F_ITALIC); - } - - public boolean affectsUnderlined() { - return affects(F_UNDERLINED); - } - - public boolean affectsBlink() { - return affects(F_BLINK); - } - - public boolean affectsInverse() { - return affects(F_INVERSE); - } - - public boolean affectsHidden() { - return affects(F_HIDDEN); - } - - public boolean affectsStrikethrough() { - return affects(F_STRIKETHROUGH); - } - - public boolean affectsFgColor() { - return affects(MASK_FG_COLOR); - } - - public boolean affectsBgColor() { - return affects(MASK_BG_COLOR); - } - - public @NonNull Style reset() { - return DEFAULT; + public @NonNull Style bold() { + return of(state | F_BOLD); } - public @NonNull Style bold() { - return of(state | F_BOLD, mask | F_BOLD); + public boolean isFaint() { + return (state & F_FAINT) != 0; } public @NonNull Style faint() { - return of(state | F_FAINT, mask | F_FAINT); + return of(state | F_FAINT); } - public @NonNull Style normal() { - return of(state & ~(F_BOLD | F_FAINT), mask | F_BOLD | F_FAINT); + public boolean isItalic() { + return (state & F_ITALIC) != 0; } public @NonNull Style italic() { - return of(state | F_ITALIC, mask | F_ITALIC); + return of(state | F_ITALIC); } public @NonNull Style italicOff() { - return of(state & ~F_ITALIC, mask | F_ITALIC); + return of(state & ~F_ITALIC); + } + + public boolean isUnderlined() { + return (state & F_UNDERLINED) != 0; } public @NonNull Style underlined() { - return of(state | F_UNDERLINED, mask | F_UNDERLINED); + return of(state | F_UNDERLINED); } public @NonNull Style underlinedOff() { - return of(state & ~F_UNDERLINED, mask | F_UNDERLINED); + return of(state & ~F_UNDERLINED); + } + + public boolean isBlink() { + return (state & F_BLINK) != 0; } public @NonNull Style blink() { - return of(state | F_BLINK, mask | F_BLINK); + return of(state | F_BLINK); } public @NonNull Style blinkOff() { - return of(state & ~F_BLINK, mask | F_BLINK); + return of(state & ~F_BLINK); + } + + public boolean isInverse() { + return (state & F_INVERSE) != 0; } public @NonNull Style inverse() { - return of(state | F_INVERSE, mask | F_INVERSE); + return of(state | F_INVERSE); } public @NonNull Style inverseOff() { - return of(state & ~F_INVERSE, mask | F_INVERSE); + return of(state & ~F_INVERSE); + } + + public boolean isHidden() { + return (state & F_HIDDEN) != 0; } public @NonNull Style hidden() { - return of(state | F_HIDDEN, mask | F_HIDDEN); + return of(state | F_HIDDEN); } public @NonNull Style hiddenOff() { - return of(state & ~F_HIDDEN, mask | F_HIDDEN); + return of(state & ~F_HIDDEN); + } + + public boolean isStrikethrough() { + return (state & F_STRIKETHROUGH) != 0; } public @NonNull Style strikethrough() { - return of(state | F_STRIKETHROUGH, mask | F_STRIKETHROUGH); + return of(state | F_STRIKETHROUGH); } public @NonNull Style strikethroughOff() { - return of(state & ~F_STRIKETHROUGH, mask | F_STRIKETHROUGH); + return of(state & ~F_STRIKETHROUGH); + } + + public @NonNull Color fgColor() { + long fgc = ((state & MASK_FG_COLOR) >> SHIFT_FG_COLOR); + return decodeColor(fgc); } public @NonNull Style fgColor(@NonNull Color color) { - long newState = applyFgColor(state, color); - return of(newState, mask | MASK_FG_COLOR); + long newState = (state & ~MASK_FG_COLOR) | (encodeColor(color) << SHIFT_FG_COLOR); + return of(newState); + } + + public @NonNull Color bgColor() { + long bgc = ((state & MASK_BG_COLOR) >> SHIFT_BG_COLOR); + return decodeColor(bgc); } public @NonNull Style bgColor(@NonNull Color color) { - long newState = applyBgColor(state, color); - return of(newState, mask | MASK_BG_COLOR); + long newState = (state & ~MASK_BG_COLOR) | (encodeColor(color) << SHIFT_BG_COLOR); + return of(newState); } private static long encodeColor(@NonNull Color color) { @@ -433,6 +272,7 @@ private static long encodeColor(@NonNull Color color) { intensity = 0; break; } + ; result |= ((long) intensity & MASK_COLOR_BASIC_INTENSITY) << SHIFT_COLOR_BASIC_INTENSITY; result |= @@ -442,7 +282,7 @@ private static long encodeColor(@NonNull Color color) { } private static @NonNull Color decodeColor(long color) { - Color result; + Color result = Color.DEFAULT; long mode = color & MASK_COLOR_MODE; if (mode == CM_INDEXED) { long paletteType = (color >> SHIFT_PALETTE_TYPE) & MASK_PALETTE_TYPE; @@ -452,19 +292,15 @@ private static long encodeColor(@NonNull Color color) { int colorIndex = (int) ((color >> SHIFT_COLOR_BASIC_INDEX) & MASK_COLOR_BASIC_INDEX); switch (intensity) { - case INTENSITY_NORMAL: + case 1: result = Color.basic(colorIndex, Color.BasicColor.Intensity.normal); break; - case INTENSITY_DARK: + case 2: result = Color.basic(colorIndex, Color.BasicColor.Intensity.dark); break; - case INTENSITY_BRIGHT: + case 3: result = Color.basic(colorIndex, Color.BasicColor.Intensity.bright); break; - case INTENSITY_DEFAULT: - default: - result = Color.DEFAULT; - break; } } else { // paletteType == F_PALETTE_INDEXED int colorIndex = (int) ((color >> SHIFT_COLOR_INDEXED_INDEX) & MASK_COLOR_PART); @@ -479,265 +315,131 @@ private static long encodeColor(@NonNull Color color) { return result; } - public static boolean isStyleSequence(@NonNull String ansiSequence) { - return ansiSequence.startsWith(Constants.CSI) && ansiSequence.endsWith("m"); - } - - public static Style parse(@NonNull String ansiSequence) { - return parse(UNSTYLED, ansiSequence); - } - - public static Style parse(@NonNull Style startingStyle, @NonNull String ansiSequence) { - if (!isStyleSequence(ansiSequence)) { - return startingStyle; - } - - String content = ansiSequence.substring(2, ansiSequence.length() - 1); - String[] parts = content.split("[;:]", -1); - int[] codes = new int[parts.length]; - for (int i = 0; i < parts.length; i++) { - try { - // Empty parameters are assumed to be 0 otherwise parse as integer - codes[i] = parts[i].isEmpty() ? 0 : Integer.parseInt(parts[i]); - } catch (NumberFormatException e) { - codes[i] = -1; // Invalid code, will be ignored - } - } - - Style style = startingStyle; - for (int i = 0; i < codes.length; i++) { - int code = codes[i]; - switch (code) { - case -1: - // Invalid code, ignore - break; - case 0: - style = style.reset(); - break; - case 1: - style = style.bold(); - break; - case 2: - style = style.faint(); - break; - case 3: - style = style.italic(); - break; - case 4: - style = style.underlined(); - break; - case 5: - style = style.blink(); - break; - case 7: - style = style.inverse(); - break; - case 8: - style = style.hidden(); - break; - case 9: - style = style.strikethrough(); - break; - case 22: - style = style.normal(); - break; - case 23: - style = style.italicOff(); - break; - case 24: - style = style.underlinedOff(); - break; - case 25: - style = style.blinkOff(); - break; - case 27: - style = style.inverseOff(); - break; - case 28: - style = style.hiddenOff(); - break; - case 29: - style = style.strikethroughOff(); - break; - case 39: - style = style.fgColor(Color.DEFAULT); - break; - case 49: - style = style.bgColor(Color.DEFAULT); - break; - default: - if (code >= 30 && code <= 37) { - Color c = Color.basic(code - 30, Color.BasicColor.Intensity.normal); - style = style.fgColor(c); - } else if (code >= 90 && code <= 97) { - Color c = Color.basic(code - 90, Color.BasicColor.Intensity.bright); - style = style.fgColor(c); - } else if (code >= 40 && code <= 47) { - Color c = Color.basic(code - 40, Color.BasicColor.Intensity.normal); - style = style.bgColor(c); - } else if (code >= 100 && code <= 107) { - Color c = Color.basic(code - 100, Color.BasicColor.Intensity.bright); - style = style.bgColor(c); - } else if (code == 38 || code == 48) { - boolean isFg = (code == 38); - if (i + 1 < codes.length) { - int type = codes[i + 1]; - if (type == 5 && i + 2 < codes.length) { - Color c = Color.indexed(codes[i + 2]); - if (isFg) { - style = style.fgColor(c); - } else { - style = style.bgColor(c); - } - i += 2; - } else if (type == 2 && i + 4 < codes.length) { - Color c = Color.rgb(codes[i + 2], codes[i + 3], codes[i + 4]); - if (isFg) { - style = style.fgColor(c); - } else { - style = style.bgColor(c); - } - i += 4; - } - } - } - break; - } - } - return style; - } - - private static long applyFgColor(long state, Color color) { - long encoded = encodeColor(color); - return (state & ~MASK_FG_COLOR) | (encoded << SHIFT_FG_COLOR); - } - - private static long applyBgColor(long state, Color color) { - long encoded = encodeColor(color); - return (state & ~MASK_BG_COLOR) | (encoded << SHIFT_BG_COLOR); - } - @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Style)) return false; Style other = (Style) o; - return this.state == other.state && this.mask == other.mask; + return this.state == other.state; } @Override public int hashCode() { - return Long.hashCode(state) * 31 + Long.hashCode(mask); + return Long.hashCode(state); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("StyleImpl{"); - if (state == F_UNKNOWN) { - sb.append("UNKNOWN}"); - return sb.toString(); - } - if (this.equals(DEFAULT)) { - sb.append("DEFAULT}"); - return sb.toString(); - } - if (affectsBold() && affectsFaint() && !isBold() && !isFaint()) { - sb.append("normal, "); - } else { - if (affectsBold()) sb.append(isBold() ? "bold, " : "-bold, "); - if (affectsFaint()) sb.append(isFaint() ? "faint, " : "-faint, "); - } - if (affectsItalic()) sb.append(isItalic() ? "italic, " : "-italic, "); - if (affectsUnderlined()) sb.append(isUnderlined() ? "underlined, " : "-underlined, "); - if (affectsBlink()) sb.append(isBlink() ? "blink, " : "-blink, "); - if (affectsInverse()) sb.append(isInverse() ? "inverse, " : "-inverse, "); - if (affectsHidden()) sb.append(isHidden() ? "hidden, " : "-hidden, "); - if (affectsStrikethrough()) - sb.append(isStrikethrough() ? "strikethrough, " : "-strikethrough, "); - if (affectsFgColor()) sb.append("fgColor=").append(fgColor()).append(", "); - if (affectsBgColor()) sb.append("bgColor=").append(bgColor()); + if (isBold()) sb.append("bold, "); + if (isFaint()) sb.append("faint, "); + if (isItalic()) sb.append("italic, "); + if (isUnderlined()) sb.append("underlined, "); + if (isBlink()) sb.append("blink, "); + if (isInverse()) sb.append("inverse, "); + if (isHidden()) sb.append("hidden, "); + if (isStrikethrough()) sb.append("strikethrough, "); + if (fgColor() != Color.DEFAULT) sb.append("fgColor=").append(fgColor()).append(", "); + if (bgColor() != Color.DEFAULT) sb.append("bgColor=").append(bgColor()); if (sb.charAt(sb.length() - 2) == ',') sb.setLength(sb.length() - 2); // Remove trailing comma sb.append('}'); return sb.toString(); } - public @NonNull Appendable toAnsi(@NonNull Appendable appendable, @NonNull Style currentStyle) { - if (this.equals(UNKNOWN)) { - // Do nothing, we keep the current state - return appendable; - } + public String toAnsiString() { + return toAnsiString(UNSTYLED); + } + + public StringBuilder toAnsiString(StringBuilder sb) { + return toAnsiString(sb, Style.UNSTYLED); + } + + public String toAnsiString(Style currentStyle) { + return toAnsiString(currentStyle.state()); + } + + public StringBuilder toAnsiString(StringBuilder sb, Style currentStyle) { + return toAnsiString(sb, currentStyle.state()); + } + + public String toAnsiString(long currentStyleState) { + return toAnsiString(new StringBuilder(), currentStyleState).toString(); + } + + public StringBuilder toAnsiString(StringBuilder sb, long currentStyleState) { List styles = new ArrayList<>(); - if (shouldApply(currentStyle, F_BOLD) || shouldApply(currentStyle, F_FAINT)) { - boolean normal = false; - if (!currentStyle.equals(UNKNOWN) - && ((!isBold() && currentStyle.isBold()) - || (!isFaint() && currentStyle.isFaint()))) { - // First we switch to NORMAL to clear both BOLD and FAINT - styles.add(Constants.NORMAL); - normal = true; + if ((currentStyleState & (F_BOLD | F_FAINT)) != (state & (F_BOLD | F_FAINT))) { + // First we switch to NORMAL to clear both BOLD and FAINT + if ((currentStyleState & (F_BOLD | F_FAINT)) != 0) { + styles.add(Ansi.NORMAL); } // Now we set the needed styles - if (isBold() && (normal || !currentStyle.affectsBold() || !currentStyle.isBold())) - styles.add(Constants.BOLD); - if (isFaint() && (normal || !currentStyle.affectsFaint() || !currentStyle.isFaint())) - styles.add(Constants.FAINT); + if (isBold()) styles.add(Ansi.BOLD); + if (isFaint()) styles.add(Ansi.FAINT); } - if (shouldApply(currentStyle, F_ITALIC)) { + if ((currentStyleState & F_ITALIC) != (state & F_ITALIC)) { if (isItalic()) { - styles.add(Constants.ITALICIZED); + styles.add(Ansi.ITALICIZED); } else { - styles.add(Constants.NOTITALICIZED); + styles.add(Ansi.NOTITALICIZED); } } - if (shouldApply(currentStyle, F_UNDERLINED)) { + if ((currentStyleState & F_UNDERLINED) != (state & F_UNDERLINED)) { if (isUnderlined()) { - styles.add(Constants.UNDERLINED); + styles.add(Ansi.UNDERLINED); } else { - styles.add(Constants.NOTUNDERLINED); + styles.add(Ansi.NOTUNDERLINED); } } - if (shouldApply(currentStyle, F_BLINK)) { + if ((currentStyleState & F_BLINK) != (state & F_BLINK)) { if (isBlink()) { - styles.add(Constants.BLINK); + styles.add(Ansi.BLINK); } else { - styles.add(Constants.STEADY); + styles.add(Ansi.STEADY); } } - if (shouldApply(currentStyle, F_INVERSE)) { + if ((currentStyleState & F_INVERSE) != (state & F_INVERSE)) { if (isInverse()) { - styles.add(Constants.INVERSE); + styles.add(Ansi.INVERSE); } else { - styles.add(Constants.POSITIVE); + styles.add(Ansi.POSITIVE); } } - if (shouldApply(currentStyle, F_HIDDEN)) { + if ((currentStyleState & F_HIDDEN) != (state & F_HIDDEN)) { if (isHidden()) { - styles.add(Constants.INVISIBLE); + styles.add(Ansi.INVISIBLE); } else { - styles.add(Constants.VISIBLE); + styles.add(Ansi.VISIBLE); } } - if (shouldApply(currentStyle, F_STRIKETHROUGH)) { + if ((currentStyleState & F_STRIKETHROUGH) != (state & F_STRIKETHROUGH)) { if (isStrikethrough()) { - styles.add(Constants.CROSSEDOUT); + styles.add(Ansi.CROSSEDOUT); } else { - styles.add(Constants.NOTCROSSEDOUT); + styles.add(Ansi.NOTCROSSEDOUT); } } - if (affectsFgColor() - && (!currentStyle.affectsFgColor() || !fgColor().equals(currentStyle.fgColor()))) { - styles.add(fgColor().toAnsiFgArgs()); + if ((currentStyleState & MASK_FG_COLOR) != (state & MASK_FG_COLOR)) { + styles.add(extractSgrParams(fgColor().toAnsiFg())); } - if (affectsBgColor() - && (!currentStyle.affectsBgColor() || !bgColor().equals(currentStyle.bgColor()))) { - styles.add(bgColor().toAnsiBgArgs()); + if ((currentStyleState & MASK_BG_COLOR) != (state & MASK_BG_COLOR)) { + styles.add(extractSgrParams(bgColor().toAnsiBg())); } - return Ansi.styles(appendable, styles.toArray()); + return Ansi.style(sb, styles.toArray()); } - private boolean shouldApply(Style otherStyle, long flag) { - return affects(flag) && (!otherStyle.affects(flag) || is(flag) != otherStyle.is(flag)); + /** + * Converts a full SGR ANSI escape sequence (e.g. {@code ESC[38;2;255;0;0m}) into just the + * contained SGR parameters (e.g. {@code "38;2;255;0;0"}), so it can be safely embedded into + * another {@code ESC[...m} sequence without producing broken output. + */ + private static String extractSgrParams(String ansi) { + if (ansi == null || ansi.isEmpty()) return ""; + if (ansi.startsWith(Ansi.CSI) && ansi.endsWith("m")) { + return ansi.substring(Ansi.CSI.length(), ansi.length() - 1); + } + return ansi; } } diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/util/Printable.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/util/Printable.java deleted file mode 100644 index a336bf9..0000000 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/util/Printable.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.codejive.twinkle.ansi.util; - -import org.codejive.twinkle.ansi.Style; -import org.jspecify.annotations.NonNull; - -public interface Printable { - /** - * Converts the object to an ANSI string, including ANSI escape codes for styles. This method - * resets the current style to default at the start of the string. - * - * @return The ANSI string representation of the object. - */ - default @NonNull String toAnsi() { - return toAnsi(Style.UNKNOWN); - } - - /** - * Outputs the object as an ANSI string, including ANSI escape codes for styles. This method - * resets the current style to default at the start of the output. - * - * @param appendable The Appendable to write the ANSI output to. - * @return The Appendable passed as parameter. - */ - default @NonNull Appendable toAnsi(@NonNull Appendable appendable) { - return toAnsi(appendable, Style.UNKNOWN); - } - - /** - * Converts the object to an ANSI string, including ANSI escape codes for styles. This method - * takes into account the provided current style to generate a result that is as efficient as - * possible in terms of ANSI codes. - * - * @param currentStyle The current style to start with. - * @return The ANSI string representation of the object. - */ - default @NonNull String toAnsi(@NonNull Style currentStyle) { - return toAnsi(new StringBuilder(), currentStyle).toString(); - } - - /** - * Outputs the object as an ANSI string, including ANSI escape codes for styles. This method - * takes into account the provided current style to generate a result that is as efficient as - * possible in terms of ANSI codes. - * - * @param appendable The Appendable to write the ANSI output to. - * @param currentStyle The current style to start with. - * @return The Appendable passed as parameter. - */ - @NonNull Appendable toAnsi(@NonNull Appendable appendable, @NonNull Style currentStyle); -} diff --git a/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestColor.java b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestColor.java index 3c6389f..f80cb86 100644 --- a/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestColor.java +++ b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestColor.java @@ -1,6 +1,7 @@ package org.codejive.twinkle.ansi; import static org.assertj.core.api.Assertions.*; +import static org.codejive.twinkle.ansi.Ansi.*; import org.codejive.twinkle.ansi.Color.*; import org.junit.jupiter.api.Test; @@ -9,132 +10,120 @@ public class TestColor { @Test public void testDefaultColorCodes() { - assertThat(Color.DEFAULT.toAnsiFg()).isEqualTo(Ansi.styles(Constants.DEFAULT_FOREGROUND)); - assertThat(Color.DEFAULT.toAnsiBg()).isEqualTo(Ansi.styles(Constants.DEFAULT_BACKGROUND)); + assertThat(Color.DEFAULT.toAnsiFg()).isEqualTo(Ansi.style(Ansi.DEFAULT_FOREGROUND)); + assertThat(Color.DEFAULT.toAnsiBg()).isEqualTo(Ansi.style(Ansi.DEFAULT_BACKGROUND)); } @Test public void testBasicColorCodes() { // Basic foreground colors - assertThat(BasicColor.BLACK.toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundArg(Constants.BLACK))); - assertThat(BasicColor.RED.toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundArg(Constants.RED))); - assertThat(BasicColor.GREEN.toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundArg(Constants.GREEN))); + assertThat(BasicColor.BLACK.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.BLACK))); + assertThat(BasicColor.RED.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.RED))); + assertThat(BasicColor.GREEN.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.GREEN))); assertThat(BasicColor.YELLOW.toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundArg(Constants.YELLOW))); - assertThat(BasicColor.BLUE.toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundArg(Constants.BLUE))); + .isEqualTo(Ansi.style(Ansi.foreground(Ansi.YELLOW))); + assertThat(BasicColor.BLUE.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.BLUE))); assertThat(BasicColor.MAGENTA.toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundArg(Constants.MAGENTA))); - assertThat(BasicColor.CYAN.toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundArg(Constants.CYAN))); - assertThat(BasicColor.WHITE.toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundArg(Constants.WHITE))); + .isEqualTo(Ansi.style(Ansi.foreground(Ansi.MAGENTA))); + assertThat(BasicColor.CYAN.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.CYAN))); + assertThat(BasicColor.WHITE.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foreground(Ansi.WHITE))); // Basic foreground colors - dark variants assertThat(BasicColor.BLACK.dark().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundDarkArg(Constants.BLACK))); + .isEqualTo(Ansi.style(Ansi.foregroundDark(BLACK))); assertThat(BasicColor.RED.dark().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundDarkArg(Constants.RED))); + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.RED))); assertThat(BasicColor.GREEN.dark().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundDarkArg(Constants.GREEN))); + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.GREEN))); assertThat(BasicColor.YELLOW.dark().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundDarkArg(Constants.YELLOW))); + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.YELLOW))); assertThat(BasicColor.BLUE.dark().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundDarkArg(Constants.BLUE))); + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.BLUE))); assertThat(BasicColor.MAGENTA.dark().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundDarkArg(Constants.MAGENTA))); + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.MAGENTA))); assertThat(BasicColor.CYAN.dark().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundDarkArg(Constants.CYAN))); + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.CYAN))); assertThat(BasicColor.WHITE.dark().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundDarkArg(Constants.WHITE))); + .isEqualTo(Ansi.style(Ansi.foregroundDark(Ansi.WHITE))); // Basic foreground colors - bright variants assertThat(BasicColor.BLACK.bright().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundBrightArg(Constants.BLACK))); + .isEqualTo(Ansi.style(Ansi.foregroundBright(BLACK))); assertThat(BasicColor.RED.bright().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundBrightArg(Constants.RED))); + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.RED))); assertThat(BasicColor.GREEN.bright().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundBrightArg(Constants.GREEN))); + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.GREEN))); assertThat(BasicColor.YELLOW.bright().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundBrightArg(Constants.YELLOW))); + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.YELLOW))); assertThat(BasicColor.BLUE.bright().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundBrightArg(Constants.BLUE))); + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.BLUE))); assertThat(BasicColor.MAGENTA.bright().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundBrightArg(Constants.MAGENTA))); + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.MAGENTA))); assertThat(BasicColor.CYAN.bright().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundBrightArg(Constants.CYAN))); + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.CYAN))); assertThat(BasicColor.WHITE.bright().toAnsiFg()) - .isEqualTo(Ansi.styles(Ansi.foregroundBrightArg(Constants.WHITE))); + .isEqualTo(Ansi.style(Ansi.foregroundBright(Ansi.WHITE))); // Basic background colors - assertThat(BasicColor.BLACK.toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundArg(Constants.BLACK))); - assertThat(BasicColor.RED.toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundArg(Constants.RED))); - assertThat(BasicColor.GREEN.toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundArg(Constants.GREEN))); + assertThat(BasicColor.BLACK.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.BLACK))); + assertThat(BasicColor.RED.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.RED))); + assertThat(BasicColor.GREEN.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.GREEN))); assertThat(BasicColor.YELLOW.toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundArg(Constants.YELLOW))); - assertThat(BasicColor.BLUE.toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundArg(Constants.BLUE))); + .isEqualTo(Ansi.style(Ansi.background(Ansi.YELLOW))); + assertThat(BasicColor.BLUE.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.BLUE))); assertThat(BasicColor.MAGENTA.toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundArg(Constants.MAGENTA))); - assertThat(BasicColor.CYAN.toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundArg(Constants.CYAN))); - assertThat(BasicColor.WHITE.toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundArg(Constants.WHITE))); + .isEqualTo(Ansi.style(Ansi.background(Ansi.MAGENTA))); + assertThat(BasicColor.CYAN.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.CYAN))); + assertThat(BasicColor.WHITE.toAnsiBg()).isEqualTo(Ansi.style(Ansi.background(Ansi.WHITE))); // Basic background colors - dark variants assertThat(BasicColor.BLACK.dark().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundDarkArg(Constants.BLACK))); + .isEqualTo(Ansi.style(Ansi.backgroundDark(BLACK))); assertThat(BasicColor.RED.dark().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundDarkArg(Constants.RED))); + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.RED))); assertThat(BasicColor.GREEN.dark().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundDarkArg(Constants.GREEN))); + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.GREEN))); assertThat(BasicColor.YELLOW.dark().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundDarkArg(Constants.YELLOW))); + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.YELLOW))); assertThat(BasicColor.BLUE.dark().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundDarkArg(Constants.BLUE))); + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.BLUE))); assertThat(BasicColor.MAGENTA.dark().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundDarkArg(Constants.MAGENTA))); + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.MAGENTA))); assertThat(BasicColor.CYAN.dark().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundDarkArg(Constants.CYAN))); + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.CYAN))); assertThat(BasicColor.WHITE.dark().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundDarkArg(Constants.WHITE))); + .isEqualTo(Ansi.style(Ansi.backgroundDark(Ansi.WHITE))); // Basic background colors - bright variants assertThat(BasicColor.BLACK.bright().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundBrightArg(Constants.BLACK))); + .isEqualTo(Ansi.style(Ansi.backgroundBright(BLACK))); assertThat(BasicColor.RED.bright().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundBrightArg(Constants.RED))); + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.RED))); assertThat(BasicColor.GREEN.bright().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundBrightArg(Constants.GREEN))); + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.GREEN))); assertThat(BasicColor.YELLOW.bright().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundBrightArg(Constants.YELLOW))); + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.YELLOW))); assertThat(BasicColor.BLUE.bright().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundBrightArg(Constants.BLUE))); + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.BLUE))); assertThat(BasicColor.MAGENTA.bright().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundBrightArg(Constants.MAGENTA))); + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.MAGENTA))); assertThat(BasicColor.CYAN.bright().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundBrightArg(Constants.CYAN))); + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.CYAN))); assertThat(BasicColor.WHITE.bright().toAnsiBg()) - .isEqualTo(Ansi.styles(Ansi.backgroundBrightArg(Constants.WHITE))); + .isEqualTo(Ansi.style(Ansi.backgroundBright(Ansi.WHITE))); } @Test public void testIndexedColorCodes() { IndexedColor color = IndexedColor.of(0); - assertThat(color.toAnsiFg()).isEqualTo(Ansi.styles(Ansi.foregroundIndexedArg(0))); - assertThat(color.toAnsiBg()).isEqualTo(Ansi.styles(Ansi.backgroundIndexedArg(0))); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundIndexed(0))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundIndexed(0))); color = IndexedColor.of(128); - assertThat(color.toAnsiFg()).isEqualTo(Ansi.styles(Ansi.foregroundIndexedArg(128))); - assertThat(color.toAnsiBg()).isEqualTo(Ansi.styles(Ansi.backgroundIndexedArg(128))); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundIndexed(128))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundIndexed(128))); color = IndexedColor.of(255); - assertThat(color.toAnsiFg()).isEqualTo(Ansi.styles(Ansi.foregroundIndexedArg(255))); - assertThat(color.toAnsiBg()).isEqualTo(Ansi.styles(Ansi.backgroundIndexedArg(255))); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundIndexed(255))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundIndexed(255))); } @Test @@ -160,14 +149,14 @@ public void testIndexedColorCodesOverflow() { @Test public void testRgbColorCodes() { RgbColor color = RgbColor.of(0, 0, 0); - assertThat(color.toAnsiFg()).isEqualTo(Ansi.styles(Ansi.foregroundRgbArg(0, 0, 0))); - assertThat(color.toAnsiBg()).isEqualTo(Ansi.styles(Ansi.backgroundRgbArg(0, 0, 0))); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundRgb(0, 0, 0))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundRgb(0, 0, 0))); color = RgbColor.of(128, 64, 32); - assertThat(color.toAnsiFg()).isEqualTo(Ansi.styles(Ansi.foregroundRgbArg(128, 64, 32))); - assertThat(color.toAnsiBg()).isEqualTo(Ansi.styles(Ansi.backgroundRgbArg(128, 64, 32))); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundRgb(128, 64, 32))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundRgb(128, 64, 32))); color = RgbColor.of(255, 255, 255); - assertThat(color.toAnsiFg()).isEqualTo(Ansi.styles(Ansi.foregroundRgbArg(255, 255, 255))); - assertThat(color.toAnsiBg()).isEqualTo(Ansi.styles(Ansi.backgroundRgbArg(255, 255, 255))); + assertThat(color.toAnsiFg()).isEqualTo(Ansi.style(Ansi.foregroundRgb(255, 255, 255))); + assertThat(color.toAnsiBg()).isEqualTo(Ansi.style(Ansi.backgroundRgb(255, 255, 255))); } @Test @@ -216,73 +205,72 @@ public void testRgbColorCodesOverflow() { @Test public void testBasicColorCodesByIndex() { - assertThat(BasicColor.byIndex(Constants.BLACK)).isEqualTo(BasicColor.BLACK); - assertThat(BasicColor.byIndex(Constants.RED)).isEqualTo(BasicColor.RED); - assertThat(BasicColor.byIndex(Constants.GREEN)).isEqualTo(BasicColor.GREEN); - assertThat(BasicColor.byIndex(Constants.YELLOW)).isEqualTo(BasicColor.YELLOW); - assertThat(BasicColor.byIndex(Constants.BLUE)).isEqualTo(BasicColor.BLUE); - assertThat(BasicColor.byIndex(Constants.MAGENTA)).isEqualTo(BasicColor.MAGENTA); - assertThat(BasicColor.byIndex(Constants.CYAN)).isEqualTo(BasicColor.CYAN); - assertThat(BasicColor.byIndex(Constants.WHITE)).isEqualTo(BasicColor.WHITE); + assertThat(BasicColor.byIndex(BLACK)).isEqualTo(BasicColor.BLACK); + assertThat(BasicColor.byIndex(RED)).isEqualTo(BasicColor.RED); + assertThat(BasicColor.byIndex(GREEN)).isEqualTo(BasicColor.GREEN); + assertThat(BasicColor.byIndex(YELLOW)).isEqualTo(BasicColor.YELLOW); + assertThat(BasicColor.byIndex(BLUE)).isEqualTo(BasicColor.BLUE); + assertThat(BasicColor.byIndex(MAGENTA)).isEqualTo(BasicColor.MAGENTA); + assertThat(BasicColor.byIndex(CYAN)).isEqualTo(BasicColor.CYAN); + assertThat(BasicColor.byIndex(WHITE)).isEqualTo(BasicColor.WHITE); } @Test public void testBasicColorCodesByIndexNormal() { - assertThat(BasicColor.byIndex(Constants.BLACK, BasicColor.Intensity.normal)) + assertThat(BasicColor.byIndex(BLACK, BasicColor.Intensity.normal)) .isEqualTo(BasicColor.BLACK); - assertThat(BasicColor.byIndex(Constants.RED, BasicColor.Intensity.normal)) - .isEqualTo(BasicColor.RED); - assertThat(BasicColor.byIndex(Constants.GREEN, BasicColor.Intensity.normal)) + assertThat(BasicColor.byIndex(RED, BasicColor.Intensity.normal)).isEqualTo(BasicColor.RED); + assertThat(BasicColor.byIndex(GREEN, BasicColor.Intensity.normal)) .isEqualTo(BasicColor.GREEN); - assertThat(BasicColor.byIndex(Constants.YELLOW, BasicColor.Intensity.normal)) + assertThat(BasicColor.byIndex(YELLOW, BasicColor.Intensity.normal)) .isEqualTo(BasicColor.YELLOW); - assertThat(BasicColor.byIndex(Constants.BLUE, BasicColor.Intensity.normal)) + assertThat(BasicColor.byIndex(BLUE, BasicColor.Intensity.normal)) .isEqualTo(BasicColor.BLUE); - assertThat(BasicColor.byIndex(Constants.MAGENTA, BasicColor.Intensity.normal)) + assertThat(BasicColor.byIndex(MAGENTA, BasicColor.Intensity.normal)) .isEqualTo(BasicColor.MAGENTA); - assertThat(BasicColor.byIndex(Constants.CYAN, BasicColor.Intensity.normal)) + assertThat(BasicColor.byIndex(CYAN, BasicColor.Intensity.normal)) .isEqualTo(BasicColor.CYAN); - assertThat(BasicColor.byIndex(Constants.WHITE, BasicColor.Intensity.normal)) + assertThat(BasicColor.byIndex(WHITE, BasicColor.Intensity.normal)) .isEqualTo(BasicColor.WHITE); } @Test public void testBasicColorCodesByIndexDark() { - assertThat(BasicColor.byIndex(Constants.BLACK, BasicColor.Intensity.dark)) + assertThat(BasicColor.byIndex(BLACK, BasicColor.Intensity.dark)) .isEqualTo(BasicColor.DARK_BLACK); - assertThat(BasicColor.byIndex(Constants.RED, BasicColor.Intensity.dark)) + assertThat(BasicColor.byIndex(RED, BasicColor.Intensity.dark)) .isEqualTo(BasicColor.DARK_RED); - assertThat(BasicColor.byIndex(Constants.GREEN, BasicColor.Intensity.dark)) + assertThat(BasicColor.byIndex(GREEN, BasicColor.Intensity.dark)) .isEqualTo(BasicColor.DARK_GREEN); - assertThat(BasicColor.byIndex(Constants.YELLOW, BasicColor.Intensity.dark)) + assertThat(BasicColor.byIndex(YELLOW, BasicColor.Intensity.dark)) .isEqualTo(BasicColor.DARK_YELLOW); - assertThat(BasicColor.byIndex(Constants.BLUE, BasicColor.Intensity.dark)) + assertThat(BasicColor.byIndex(BLUE, BasicColor.Intensity.dark)) .isEqualTo(BasicColor.DARK_BLUE); - assertThat(BasicColor.byIndex(Constants.MAGENTA, BasicColor.Intensity.dark)) + assertThat(BasicColor.byIndex(MAGENTA, BasicColor.Intensity.dark)) .isEqualTo(BasicColor.DARK_MAGENTA); - assertThat(BasicColor.byIndex(Constants.CYAN, BasicColor.Intensity.dark)) + assertThat(BasicColor.byIndex(CYAN, BasicColor.Intensity.dark)) .isEqualTo(BasicColor.DARK_CYAN); - assertThat(BasicColor.byIndex(Constants.WHITE, BasicColor.Intensity.dark)) + assertThat(BasicColor.byIndex(WHITE, BasicColor.Intensity.dark)) .isEqualTo(BasicColor.DARK_WHITE); } @Test public void testBasicColorCodesByIndexBright() { - assertThat(BasicColor.byIndex(Constants.BLACK, BasicColor.Intensity.bright)) + assertThat(BasicColor.byIndex(BLACK, BasicColor.Intensity.bright)) .isEqualTo(BasicColor.BRIGHT_BLACK); - assertThat(BasicColor.byIndex(Constants.RED, BasicColor.Intensity.bright)) + assertThat(BasicColor.byIndex(RED, BasicColor.Intensity.bright)) .isEqualTo(BasicColor.BRIGHT_RED); - assertThat(BasicColor.byIndex(Constants.GREEN, BasicColor.Intensity.bright)) + assertThat(BasicColor.byIndex(GREEN, BasicColor.Intensity.bright)) .isEqualTo(BasicColor.BRIGHT_GREEN); - assertThat(BasicColor.byIndex(Constants.YELLOW, BasicColor.Intensity.bright)) + assertThat(BasicColor.byIndex(YELLOW, BasicColor.Intensity.bright)) .isEqualTo(BasicColor.BRIGHT_YELLOW); - assertThat(BasicColor.byIndex(Constants.BLUE, BasicColor.Intensity.bright)) + assertThat(BasicColor.byIndex(BLUE, BasicColor.Intensity.bright)) .isEqualTo(BasicColor.BRIGHT_BLUE); - assertThat(BasicColor.byIndex(Constants.MAGENTA, BasicColor.Intensity.bright)) + assertThat(BasicColor.byIndex(MAGENTA, BasicColor.Intensity.bright)) .isEqualTo(BasicColor.BRIGHT_MAGENTA); - assertThat(BasicColor.byIndex(Constants.CYAN, BasicColor.Intensity.bright)) + assertThat(BasicColor.byIndex(CYAN, BasicColor.Intensity.bright)) .isEqualTo(BasicColor.BRIGHT_CYAN); - assertThat(BasicColor.byIndex(Constants.WHITE, BasicColor.Intensity.bright)) + assertThat(BasicColor.byIndex(WHITE, BasicColor.Intensity.bright)) .isEqualTo(BasicColor.BRIGHT_WHITE); } } diff --git a/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java index b68caa8..980fadb 100644 --- a/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java +++ b/twinkle-ansi/src/test/java/org/codejive/twinkle/ansi/TestStyle.java @@ -7,7 +7,7 @@ public class TestStyle { @Test public void testStyleCreation() { - Style style1 = Style.UNSTYLED.bold(); + Style style1 = Style.of(Style.F_BOLD); Style style2 = Style.BOLD; assertThat(style1).isEqualTo(style2); @@ -30,14 +30,6 @@ public void testStyleCombination() { .inverse() .hidden() .strikethrough(); - assertThat(style.affectsBold()).isTrue(); - assertThat(style.affectsFaint()).isTrue(); - assertThat(style.affectsItalic()).isTrue(); - assertThat(style.affectsUnderlined()).isTrue(); - assertThat(style.affectsBlink()).isTrue(); - assertThat(style.affectsInverse()).isTrue(); - assertThat(style.affectsHidden()).isTrue(); - assertThat(style.affectsStrikethrough()).isTrue(); assertThat(style.isBold()).isTrue(); assertThat(style.isFaint()).isTrue(); assertThat(style.isItalic()).isTrue(); @@ -55,56 +47,7 @@ public void testStyleCombination() { .inverseOff() .hiddenOff() .strikethroughOff(); - assertThat(style.affectsBold()).isTrue(); - assertThat(style.affectsFaint()).isTrue(); - assertThat(style.affectsItalic()).isTrue(); - assertThat(style.affectsUnderlined()).isTrue(); - assertThat(style.affectsBlink()).isTrue(); - assertThat(style.affectsInverse()).isTrue(); - assertThat(style.affectsHidden()).isTrue(); - assertThat(style.affectsStrikethrough()).isTrue(); - assertThat(style.isBold()).isFalse(); - assertThat(style.isFaint()).isFalse(); - assertThat(style.isItalic()).isFalse(); - assertThat(style.isUnderlined()).isFalse(); - assertThat(style.isBlink()).isFalse(); - assertThat(style.isInverse()).isFalse(); - assertThat(style.isHidden()).isFalse(); - assertThat(style.isStrikethrough()).isFalse(); - } - - @Test - public void testUnsetStyle() { - Style style = Style.UNSTYLED.underlinedOff(); - assertThat(style.affectsUnderlined()).isTrue(); - assertThat(style.isUnderlined()).isFalse(); - assertThat(style.toAnsi()).isEqualTo(Ansi.styles(Constants.NOTUNDERLINED)); - } - - @Test - public void testUnsetStyleAnd() { - Style style1 = Style.UNSTYLED.blink().underlined(); - Style style2 = Style.UNSTYLED.underlinedOff(); - - Style style3 = style1.and(style2); - - assertThat(style3.affectsBlink()).isTrue(); - assertThat(style3.isBlink()).isTrue(); - assertThat(style3.affectsUnderlined()).isTrue(); - assertThat(style3.isUnderlined()).isFalse(); - } - - @Test - public void testUnsetStyleApply() { - Style style1 = Style.UNSTYLED.blink().underlined(); - Style style2 = Style.UNSTYLED.underlinedOff(); - - Style style3 = style1.apply(style2); - - assertThat(style3.affectsBlink()).isTrue(); - assertThat(style3.isBlink()).isTrue(); - assertThat(style3.affectsUnderlined()).isFalse(); - assertThat(style3.isUnderlined()).isFalse(); + assertThat(style).isEqualTo(Style.UNSTYLED); } @Test @@ -148,9 +91,9 @@ public void testMixedStyles() { .blink() .inverse() .hidden() - .strikethrough() - .fgColor(Color.BasicColor.BLUE) - .bgColor(Color.indexed(128)); + .strikethrough(); + style = style.fgColor(Color.BasicColor.BLUE); + style = style.bgColor(Color.indexed(128)); style = style.normal() .italicOff() @@ -158,45 +101,16 @@ public void testMixedStyles() { .blinkOff() .inverseOff() .hiddenOff() - .strikethroughOff() - .fgColor(Color.DEFAULT) - .bgColor(Color.DEFAULT); - assertThat(style).isEqualTo(Style.DEFAULT); - } - - @Test - public void testMixedStylesApply() { - Style style = - Style.UNSTYLED - .bold() - .faint() - .italic() - .underlined() - .blink() - .inverse() - .hidden() - .strikethrough() - .fgColor(Color.BasicColor.BLUE) - .bgColor(Color.indexed(128)); - style = - style.apply( - Style.UNSTYLED - .normal() - .italicOff() - .underlinedOff() - .blinkOff() - .inverseOff() - .hiddenOff() - .strikethroughOff() - .fgColor(Color.DEFAULT) - .bgColor(Color.DEFAULT)); + .strikethroughOff(); + style = style.fgColor(Color.DEFAULT); + style = style.bgColor(Color.DEFAULT); assertThat(style).isEqualTo(Style.UNSTYLED); } @Test public void testToAnsiStringUnstyled() { Style style = Style.UNSTYLED; - String ansiCode = style.toAnsi(); + String ansiCode = style.toAnsiString(); assertThat(ansiCode).isEqualTo(""); } @@ -212,44 +126,18 @@ public void testToAnsiStringAllStyles() { .inverse() .hidden() .strikethrough(); - String ansiCode = style.toAnsi(); + String ansiCode = style.toAnsiString(); assertThat(ansiCode) .isEqualTo( - Ansi.styles( - Constants.BOLD, - Constants.FAINT, - Constants.ITALICIZED, - Constants.UNDERLINED, - Constants.BLINK, - Constants.INVERSE, - Constants.INVISIBLE, - Constants.CROSSEDOUT)); - } - - @Test - public void testToAnsiStringAllStylesWithDefault() { - Style style = - Style.UNSTYLED - .bold() - .faint() - .italic() - .underlined() - .blink() - .inverse() - .hidden() - .strikethrough(); - String ansiCode = style.toAnsi(Style.DEFAULT); - assertThat(ansiCode) - .isEqualTo( - Ansi.styles( - Constants.BOLD, - Constants.FAINT, - Constants.ITALICIZED, - Constants.UNDERLINED, - Constants.BLINK, - Constants.INVERSE, - Constants.INVISIBLE, - Constants.CROSSEDOUT)); + Ansi.style( + Ansi.BOLD, + Ansi.FAINT, + Ansi.ITALICIZED, + Ansi.UNDERLINED, + Ansi.BLINK, + Ansi.INVERSE, + Ansi.INVISIBLE, + Ansi.CROSSEDOUT)); } @Test @@ -264,17 +152,18 @@ public void testToAnsiStringAllStylesWithCurrent() { .inverse() .hidden() .strikethrough(); - Style currentStyle = Style.UNSTYLED.bold().underlined(); - String ansiCode = style.toAnsi(currentStyle); + String ansiCode = style.toAnsiString(Style.F_BOLD | Style.F_UNDERLINED); assertThat(ansiCode) .isEqualTo( - Ansi.styles( - Constants.FAINT, - Constants.ITALICIZED, - Constants.BLINK, - Constants.INVERSE, - Constants.INVISIBLE, - Constants.CROSSEDOUT)); + Ansi.style( + Ansi.NORMAL, + Ansi.BOLD, + Ansi.FAINT, + Ansi.ITALICIZED, + Ansi.BLINK, + Ansi.INVERSE, + Ansi.INVISIBLE, + Ansi.CROSSEDOUT)); } @Test @@ -289,31 +178,14 @@ public void testToAnsiStringAllStylesWithCurrent2() { .inverse() .hidden() .strikethrough(); - Style currentStyle = Style.UNSTYLED.bold().faint().underlined(); - String ansiCode = style.toAnsi(currentStyle); + String ansiCode = style.toAnsiString(Style.F_BOLD | Style.F_FAINT | Style.F_UNDERLINED); assertThat(ansiCode) .isEqualTo( - Ansi.styles( - Constants.ITALICIZED, - Constants.BLINK, - Constants.INVERSE, - Constants.INVISIBLE, - Constants.CROSSEDOUT)); - } - - @Test - public void testToAnsiStringNormal() { - Style style = Style.UNSTYLED.faint(); - Style currentStyle = Style.UNSTYLED.bold(); - String ansiCode = style.toAnsi(currentStyle); - assertThat(ansiCode).isEqualTo(Ansi.styles(Constants.NORMAL, Constants.FAINT)); - } - - @Test - public void testToAnsiStringNoNormal() { - Style style = Style.UNSTYLED.bold().faint(); - Style currentStyle = Style.UNSTYLED.bold(); - String ansiCode = style.toAnsi(currentStyle); - assertThat(ansiCode).isEqualTo(Ansi.styles(Constants.FAINT)); + Ansi.style( + Ansi.ITALICIZED, + Ansi.BLINK, + Ansi.INVERSE, + Ansi.INVISIBLE, + Ansi.CROSSEDOUT)); } } diff --git a/twinkle-chart/pom.xml b/twinkle-chart/pom.xml new file mode 100644 index 0000000..5a23f94 --- /dev/null +++ b/twinkle-chart/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + org.codejive.twinkle + twinkle + 1.0-SNAPSHOT + ../pom.xml + + + twinkle-chart + jar + + Text-mode charting module for Twinkle TUI library + + + + org.codejive.twinkle + twinkle-core + 1.0-SNAPSHOT + compile + + + + + diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/Bar.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/Bar.java new file mode 100644 index 0000000..465b398 --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/Bar.java @@ -0,0 +1,31 @@ +package org.codejive.twinkle.widgets.graphs.bar; + +import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.widget.Renderable; + +public class Bar implements Renderable { + private final FracBarRenderer renderer; + private Number value = 0.0d; + + /** + * Returns a fractional horizontal Bar representing values between 0 and 100. + * + * @return A Bar instance + */ + public static Bar bar() { + return new Bar(FracBarConfig.create()); + } + + public Bar(FracBarConfig config) { + this.renderer = new FracBarRenderer(config); + } + + public Bar setValue(Number value) { + this.value = value; + return this; + } + + public void render(Canvas canvas) { + renderer.render(canvas, value); + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/BarConfig.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/BarConfig.java new file mode 100644 index 0000000..8dc6d1a --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/BarConfig.java @@ -0,0 +1,77 @@ +package org.codejive.twinkle.widgets.graphs.bar; + +public class BarConfig { + private Number minValue; + private Number maxValue; + private Direction direction; + + public enum Orientation { + HORIZONTAL, + VERTICAL; + } + + public enum Direction { + L2R(Orientation.HORIZONTAL, 1, 0), + R2L(Orientation.HORIZONTAL, -1, 0), + B2T(Orientation.VERTICAL, 0, -1), + T2B(Orientation.VERTICAL, 0, 1); + + public final Orientation orientation; + public final int dx; + public final int dy; + + Direction(Orientation orientation, int dx, int dy) { + this.orientation = orientation; + this.dx = dx; + this.dy = dy; + } + } + + public BarConfig() { + this.minValue = 0; + this.maxValue = 100; + this.direction = Direction.L2R; + } + + public Number minValue() { + return minValue; + } + + public BarConfig minValue(Number minValue) { + this.minValue = minValue; + return this; + } + + public Number maxValue() { + return maxValue; + } + + public BarConfig maxValue(Number maxValue) { + this.maxValue = maxValue; + return this; + } + + public Direction direction() { + return direction; + } + + public BarConfig direction(Direction direction) { + this.direction = direction; + return this; + } + + public BarConfig copy() { + return copy_(new BarConfig()); + } + + protected BarConfig copy_(BarConfig b) { + b.minValue = this.minValue; + b.maxValue = this.maxValue; + b.direction = this.direction; + return b; + } + + public static BarConfig create() { + return new BarConfig(); + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarConfig.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarConfig.java new file mode 100644 index 0000000..38cd2ce --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarConfig.java @@ -0,0 +1,55 @@ +package org.codejive.twinkle.widgets.graphs.bar; + +public class FracBarConfig extends BarConfig { + private Design design; + + public enum Design { + TEXT_BLOCK, + COLOR_BLOCK, + FULL_BLOCK, + HALF_BLOCK, + FRACTIONAL_BLOCK; + } + + public FracBarConfig() { + this.design = Design.FRACTIONAL_BLOCK; + } + + @Override + public FracBarConfig minValue(Number minValue) { + return (FracBarConfig) super.minValue(minValue); + } + + @Override + public FracBarConfig maxValue(Number maxValue) { + return (FracBarConfig) super.maxValue(maxValue); + } + + @Override + public FracBarConfig direction(Direction direction) { + return (FracBarConfig) super.direction(direction); + } + + public Design design() { + return design; + } + + public FracBarConfig design(Design design) { + this.design = design; + return this; + } + + public FracBarConfig copy() { + return copy_(new FracBarConfig()); + } + + protected FracBarConfig copy_(FracBarConfig b) { + super.copy_(b); + b.design = this.design; + return b; + } + + public static FracBarConfig create() { + return new FracBarConfig(); + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarRenderer.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarRenderer.java new file mode 100644 index 0000000..9a75471 --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarRenderer.java @@ -0,0 +1,146 @@ +package org.codejive.twinkle.widgets.graphs.bar; + +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.widgets.graphs.bar.BarConfig.*; +import org.codejive.twinkle.widgets.graphs.bar.FracBarConfig.*; + +public class FracBarRenderer { + private final FracBarConfig config; + + private static final char[] TEXT_BLOCK = {'#'}; + private static final char[] COLOR_BLOCK = {' '}; + private static final char[] BLOCK_FULL = {'█'}; + private static final char[] BLOCK_HALF_L2R = {'█', '▌'}; // full, left half + private static final char[] BLOCK_HALF_R2L = {'█', '▐'}; // full, right half + private static final char[] BLOCK_HALF_T2B = {'█', '▀'}; // full, top half + private static final char[] BLOCK_HALF_B2T = {'█', '▄'}; // full, bottom half + private static final char[] BLOCK_FRAC_L2R = { + '█', '▏', '▎', '▍', '▌', '▋', '▊', '▉' + }; // full, left 1/8 .. 7/8 + private static final char[] BLOCK_FRAC_B2T = { + '█', '▁', '▂', '▃', '▄', '▅', '▆', '▇' + }; // full, lower 1/8 .. 7/8 + private static final char BLOCK_OVERFLOW = '▓'; // dark shade block + + public FracBarRenderer() { + this(FracBarConfig.create()); + } + + public FracBarRenderer(FracBarConfig config) { + this.config = config; + } + + public void render(Canvas canvas, Number value) { + FracBarConfig.Design activeDesign = config.design(); + if (activeDesign == Design.FRACTIONAL_BLOCK + && (config.direction() == Direction.R2L || config.direction() == Direction.T2B)) { + // Fallback to half block design for unsupported directions + activeDesign = Design.HALF_BLOCK; + } + + double dx = value.doubleValue(); + double dmin = config.minValue().doubleValue(); + double dmax = config.maxValue().doubleValue(); + + if (dmin > dmax) { + throw new IllegalArgumentException("Minimum value greater than maximum value"); + } + // Clamp value to minimum + dx = Math.max(dmin, dx); + + Direction direction = config.direction(); + + char[] blocks; + switch (activeDesign) { + case TEXT_BLOCK: + blocks = TEXT_BLOCK; + break; + case COLOR_BLOCK: + blocks = COLOR_BLOCK; + break; + case FULL_BLOCK: + blocks = BLOCK_FULL; + break; + case HALF_BLOCK: + switch (direction) { + case L2R: + blocks = BLOCK_HALF_L2R; + break; + case R2L: + blocks = BLOCK_HALF_R2L; + break; + case B2T: + blocks = BLOCK_HALF_B2T; + break; + case T2B: + blocks = BLOCK_HALF_T2B; + break; + default: + throw new IllegalStateException("Unknown direction: " + direction); + } + break; + case FRACTIONAL_BLOCK: + switch (direction) { + case L2R: + blocks = BLOCK_FRAC_L2R; + break; + case B2T: + blocks = BLOCK_FRAC_B2T; + break; + case R2L: + case T2B: + // We shouldn't get here because we fall back to half blocks in these cases + throw new IllegalStateException( + "Unsupported direction: " + + direction + + " for design: " + + activeDesign); + default: + throw new IllegalStateException("Unknown direction: " + direction); + } + break; + default: + throw new IllegalStateException("Unknown design: " + activeDesign); + } + + Size size = canvas.size(); + boolean reversed = direction == Direction.R2L || direction == Direction.T2B; + boolean horizontal = direction.orientation == Orientation.HORIZONTAL; + int maxSize = horizontal ? size.width() : size.height(); + int nroBlocksPerChar = blocks.length; + int maxSizeInFractions = maxSize * nroBlocksPerChar; + double interval = dmax - dmin; + int barWidthInFractions = (int) (((dx - dmin) / interval) * maxSizeInFractions); + boolean overflow = barWidthInFractions > maxSizeInFractions; + int fullChunks = overflow ? maxSize - 1 : barWidthInFractions / nroBlocksPerChar; + int remainder = overflow ? 0 : barWidthInFractions % nroBlocksPerChar; + + int x = !reversed && horizontal ? 0 : size.width() - 1; + int y = reversed || horizontal ? 0 : size.height() - 1; + // Place full blocks first + for (int i = 0; i < fullChunks; i++) { + canvas.setCharAt(x, y, Style.F_UNSTYLED, blocks[0]); + x += direction.dx; + y += direction.dy; + } + // Append remainder partial block if any + if (remainder > 0) { + canvas.setCharAt(x, y, Style.F_UNSTYLED, blocks[remainder]); + x += direction.dx; + y += direction.dy; + } else if (overflow) { // Or an overflow block + canvas.setCharAt(x, y, Style.F_UNSTYLED, BLOCK_OVERFLOW); + x += direction.dx; + y += direction.dy; + } + // Fill the rest with spaces + int sizeLeft = maxSize - fullChunks - (overflow || remainder > 0 ? 1 : 0); + for (int i = 0; i < sizeLeft; i++) { + canvas.setCharAt(x, y, Style.F_UNSTYLED, ' '); + x += direction.dx; + y += direction.dy; + } + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/MathPlot.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/MathPlot.java new file mode 100644 index 0000000..b3f1925 --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/MathPlot.java @@ -0,0 +1,271 @@ +package org.codejive.twinkle.widgets.graphs.plot; + +import java.util.function.Function; +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.core.widget.Widget; +import org.jspecify.annotations.NonNull; + +public class MathPlot implements Widget { + private final Plot plot; + private Origin origin; + private Number minXValue; + private Number maxXValue; + private Number minYValue; + private Number maxYValue; + private double originX; + private double originY; + + public enum Origin { + N, + S, + E, + W, + NE, + NW, + SE, + SW, + CENTER + } + + public static MathPlot of(Size size) { + return new MathPlot(Plot.of(size)); + } + + public static MathPlot of(Canvas canvas) { + return new MathPlot(Plot.of(canvas)); + } + + public static MathPlot of(Plot plot) { + return new MathPlot(plot); + } + + protected MathPlot(Plot plot) { + this.plot = plot; + ranges(-1.0d, 1.0d, -1.0d, 1.0d); + origin(Origin.CENTER); + } + + @Override + public @NonNull Size size() { + return plot.size(); + } + + public Origin origin() { + return origin; + } + + public MathPlot origin(Origin origin) { + this.origin = origin; + int width = plot.plotSize().width(); + int height = plot.plotSize().height(); + switch (origin) { + case N: + originX = (width - 1) / 2.0; + originY = height - 1; + break; + case S: + originX = (width - 1) / 2.0; + originY = 0.0; + break; + case E: + originX = width - 1; + originY = (height - 1) / 2.0; + break; + case W: + originX = 0.0; + originY = (height - 1) / 2.0; + break; + case NE: + originX = width - 1; + originY = height - 1; + break; + case NW: + originX = 0.0; + originY = height - 1; + break; + case SE: + originX = width - 1; + originY = 0.0; + break; + case SW: + originX = 0.0; + originY = 0.0; + break; + case CENTER: + default: + originX = (width - 1) / 2.0; + originY = (height - 1) / 2.0; + break; + } + return this; + } + + public Number getMinXValue() { + return minXValue; + } + + public MathPlot minXValue(Number minXValue) { + this.minXValue = minXValue; + return this; + } + + public Number maxXValue() { + return maxXValue; + } + + public MathPlot maxXValue(Number maxXValue) { + this.maxXValue = maxXValue; + return this; + } + + public MathPlot xRange(Number minXValue, Number maxXValue) { + this.minXValue = minXValue; + this.maxXValue = maxXValue; + return this; + } + + public Number minYValue() { + return minYValue; + } + + public MathPlot minYValue(Number minYValue) { + this.minYValue = minYValue; + return this; + } + + public Number maxYValue() { + return maxYValue; + } + + public MathPlot maxYValue(Number maxYValue) { + this.maxYValue = maxYValue; + return this; + } + + public MathPlot yRange(Number minYValue, Number maxYValue) { + this.minYValue = minYValue; + this.maxYValue = maxYValue; + return this; + } + + public MathPlot ranges(Number minXValue, Number maxXValue, Number minYValue, Number maxYValue) { + this.minXValue = minXValue; + this.maxXValue = maxXValue; + this.minYValue = minYValue; + this.maxYValue = maxYValue; + return this; + } + + public @NonNull Style currentStyle() { + return plot.currentStyle(); + } + + public long currentStyleState() { + return plot.currentStyleState(); + } + + public MathPlot currentStyle(Style currentStyle) { + plot.currentStyle(currentStyle); + return this; + } + + public MathPlot currentStyleState(long currentStyleState) { + plot.currentStyleState(currentStyleState); + return this; + } + + public MathPlot plot(Function func) { + double xRange = maxXValue.doubleValue() - minXValue.doubleValue(); + double yRange = maxYValue.doubleValue() - minYValue.doubleValue(); + Size plotSize = plot.plotSize(); + int width = plotSize.width(); + int height = plotSize.height(); + if (width <= 0 || height <= 0 || xRange <= 0.0 || yRange <= 0.0) { + return this; + } + + // Precompute default positions for zero in the numeric range (where numeric 0 maps in + // default mapping). + double defaultZeroX = (0.0 - minXValue.doubleValue()) / xRange * (width - 1); + double defaultZeroY = (0.0 - minYValue.doubleValue()) / yRange * (height - 1); + + double deltaX = originX - defaultZeroX; + double deltaY = originY - defaultZeroY; + + boolean prevValid = false; + int prevX = 0; + int prevY = 0; + + for (int xi = 0; xi < width; xi++) { + // map pixel x to numeric x in [minX, maxX] + double tX = (width == 1) ? 0.0 : (double) xi / (width - 1); + double scaledX = minXValue.doubleValue() + tX * xRange; + + double scaledY = func.apply(scaledX); + if (Double.isNaN(scaledY) || Double.isInfinite(scaledY)) { + prevValid = false; + continue; + } + + // map numeric y to pixel y in default mapping (minY -> 0, maxY -> height-1) + double defaultPixelX = (scaledX - minXValue.doubleValue()) / xRange * (width - 1); + double defaultPixelY = (scaledY - minYValue.doubleValue()) / yRange * (height - 1); + + // apply origin shift so numeric zero lands where requested + int px = (int) Math.round(defaultPixelX + deltaX); + int py = (int) Math.round(defaultPixelY + deltaY); + + boolean currValid = !(px < 0 || px >= width || py < 0 || py >= height); + + if (prevValid && currValid) { + // draw line between prev and current using Bresenham + int x0 = prevX; + int y0 = prevY; + int x1 = px; + int y1 = py; + int dx = Math.abs(x1 - x0); + int sx = x0 < x1 ? 1 : -1; + int dy = Math.abs(y1 - y0); + int sy = y0 < y1 ? 1 : -1; + int err = dx - dy; + + while (true) { + if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) { + plot.plot(x0, y0); + } + if (x0 == x1 && y0 == y1) { + break; + } + int e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + prevX = px; + prevY = py; + } else if (currValid) { + // start new segment (or single point) and record as previous + plot.plot(px, py); + prevX = px; + prevY = py; + prevValid = true; + } else { + // current invalid -> break continuity + prevValid = false; + } + } + return this; + } + + @Override + public void render(Canvas canvas) { + plot.render(canvas); + } +} diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/Plot.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/Plot.java new file mode 100644 index 0000000..aa1b034 --- /dev/null +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/Plot.java @@ -0,0 +1,175 @@ +package org.codejive.twinkle.widgets.graphs.plot; + +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.core.widget.Widget; +import org.jspecify.annotations.NonNull; + +public class Plot implements Widget { + private final Canvas canvas; + private final int cOrgX; + private final int cOrgY; + private final Size plotSize; + private long currentStyleState = Style.F_UNSTYLED; + + private static final char BLOCK_FULL = '█'; + private static final char EMPTY = ' '; + + private static final char DOT_LOWER_LEFT = '▖'; + private static final char DOT_LOWER_RIGHT = '▗'; + private static final char DOT_UPPER_LEFT = '▘'; + private static final char DOT_UPPER_RIGHT = '▝'; + + private static final char DOTS_TL_BR = '▚'; + private static final char DOTS_BL_TR = '▞'; + + private static final char BLOCK_LEFT_HALF = '▌'; // full, left half + private static final char BLOCK_RIGHT_HALF = '▐'; // full, right half + private static final char BLOCK_TOP_HALF = '▀'; // full, top half + private static final char BLOCK_BOTTOM_HALF = '▄'; // full, bottom half + + private static final char HOLE_LOWER_LEFT = '▜'; + private static final char HOLE_LOWER_RIGHT = '▛'; + private static final char HOLE_UPPER_LEFT = '▟'; + private static final char HOLE_UPPER_RIGHT = '▙'; + + private static final char[] ALL_DOTS = { + EMPTY, DOT_LOWER_LEFT, DOT_LOWER_RIGHT, BLOCK_BOTTOM_HALF, + DOT_UPPER_LEFT, BLOCK_LEFT_HALF, DOTS_TL_BR, HOLE_UPPER_RIGHT, + DOT_UPPER_RIGHT, DOTS_BL_TR, BLOCK_RIGHT_HALF, HOLE_UPPER_LEFT, + BLOCK_TOP_HALF, HOLE_LOWER_RIGHT, HOLE_LOWER_LEFT, BLOCK_FULL + }; + + private static final char[] SINGLE_DOTS = { + DOT_LOWER_LEFT, DOT_LOWER_RIGHT, DOT_UPPER_LEFT, DOT_UPPER_RIGHT + }; + + private static final int dotIndex[] = {1, 2, 4, 7, 6, 13, 14, 8, 9, 11}; + + public static Plot of(Size size) { + return new Plot(Panel.of(size)); + } + + public static Plot of(Canvas canvas) { + return new Plot(canvas); + } + + protected Plot(Canvas canvas) { + this.canvas = canvas; + this.cOrgX = 0; + this.cOrgY = canvas.size().height() - 1; + this.plotSize = Size.of(canvas.size().width() * 2, canvas.size().height() * 2); + } + + @Override + public @NonNull Size size() { + return canvas.size(); + } + + public @NonNull Size plotSize() { + return plotSize; + } + + public @NonNull Style currentStyle() { + return Style.of(currentStyleState); + } + + public long currentStyleState() { + return currentStyleState; + } + + public Plot currentStyle(Style currentStyle) { + this.currentStyleState = currentStyle.state(); + return this; + } + + public Plot currentStyleState(long currentStyleState) { + this.currentStyleState = currentStyleState; + return this; + } + + @Override + public void render(Canvas canvas) { + this.canvas.copyTo(canvas, 0, 0); + } + + public Plot plot(int x, int y) { + return plot(x, y, currentStyleState); + } + + public Plot plot(int x, int y, Style style) { + return plot(x, y, style.state()); + } + + public Plot plot(int x, int y, long styleState) { + int cx = cOrgX + x / 2; + int cy = cOrgY - y / 2; + int rx = x % 2; + int ry = y % 2; + char newDot = selectDot(rx, ry); + char existingDot = canvas.charAt(cx, cy); + char combinedDot = combineDots(existingDot, newDot); + canvas.setCharAt(cx, cy, styleState, combinedDot); + return this; + } + + public Plot unplot(int x, int y) { + int cx = cOrgX + x / 2; + int cy = cOrgY - y / 2; + int rx = x % 2; + int ry = y % 2; + char removeDot = selectDot(rx, ry); + char existingDot = canvas.charAt(cx, cy); + char combinedDot = uncombineDots(existingDot, removeDot); + canvas.setCharAt(cx, cy, currentStyleState, combinedDot); + return this; + } + + public Plot clear() { + for (int y = 0; y < size().height(); y++) { + for (int x = 0; x < size().width(); x++) { + canvas.setCharAt(x, y, currentStyleState, ' '); + } + } + return this; + } + + private char selectDot(int rx, int ry) { + int dotIdx = ry * 2 + rx; + return SINGLE_DOTS[dotIdx]; + } + + private int charToDotIndex(char c) { + if (c == '\u2584') { + return 3; + } else if (c == '\u258c') { + return 5; + } else if (c == '\u2590') { + return 10; + } else if (c == '\u2580') { + return 12; + } else if (c == '\u2588') { + return 15; + } else if (c >= '\u2596' && c <= '\u259f') { + int idx = c - '\u2596'; + return dotIndex[idx]; + } + return 0; + } + + private char combineDots(char existing, char added) { + int existingIdx = charToDotIndex(existing); + int addedIdx = charToDotIndex(added); + int combinedIdx = existingIdx | addedIdx; + return ALL_DOTS[combinedIdx]; + } + + private char uncombineDots(char existing, char removed) { + int existingIdx = charToDotIndex(existing); + int removedIdx = charToDotIndex(removed); + int combinedIdx = existingIdx & (~removedIdx); + return ALL_DOTS[combinedIdx]; + } +} diff --git a/twinkle-chart/src/test/java/examples/BarDemo.java b/twinkle-chart/src/test/java/examples/BarDemo.java new file mode 100644 index 0000000..a3b06b2 --- /dev/null +++ b/twinkle-chart/src/test/java/examples/BarDemo.java @@ -0,0 +1,65 @@ +package examples; + +import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.widgets.graphs.bar.Bar; +import org.codejive.twinkle.widgets.graphs.bar.BarConfig; +import org.codejive.twinkle.widgets.graphs.bar.FracBarConfig; + +public class BarDemo { + public static void main(String[] args) { + System.out.println("Simple Bar:"); + printSimpleBar(); + + System.out.println("Horizontal Bars:"); + printHorizontalBars(); + + System.out.println("Vertical Bars:"); + printVerticalBars(); + } + + private static void printSimpleBar() { + Panel pnl = Panel.of(20, 1); + Bar b = Bar.bar().setValue(42); + b.render(pnl); + System.out.println(pnl.toString()); + } + + private static void printHorizontalBars() { + Panel pnl = Panel.of(20, 4); + FracBarConfig cfg = FracBarConfig.create(); + renderHorizontal(pnl, cfg); + System.out.println(pnl.toString()); + + cfg.direction(BarConfig.Direction.R2L); + renderHorizontal(pnl, cfg); + System.out.println(pnl.toString()); + } + + private static void renderHorizontal(Panel pnl, FracBarConfig cfg) { + for (int i = 0; i < pnl.size().height(); i++) { + Canvas v = pnl.view(0, i, 20, 1); + Bar b = new Bar(cfg).setValue(30 + i * 27); + b.render(v); + } + } + + private static void printVerticalBars() { + Panel pnl = Panel.of(16, 8); + FracBarConfig cfg = FracBarConfig.create().direction(BarConfig.Direction.B2T); + renderVertical(pnl, cfg); + System.out.println(pnl.toString()); + + cfg.direction(BarConfig.Direction.T2B); + renderVertical(pnl, cfg); + System.out.println(pnl.toString()); + } + + private static void renderVertical(Panel pnl, FracBarConfig cfg) { + for (int i = 0; i < pnl.size().width(); i++) { + Canvas v = pnl.view(i, 0, 1, 8); + Bar b = new Bar(cfg).setValue(30 + i * 5.4d); + b.render(v); + } + } +} diff --git a/twinkle-chart/src/test/java/examples/MathPlotDemo.java b/twinkle-chart/src/test/java/examples/MathPlotDemo.java new file mode 100644 index 0000000..f9c787d --- /dev/null +++ b/twinkle-chart/src/test/java/examples/MathPlotDemo.java @@ -0,0 +1,19 @@ +package examples; + +import org.codejive.twinkle.core.text.Line; +import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.widgets.Framed; +import org.codejive.twinkle.widgets.graphs.plot.MathPlot; + +public class MathPlotDemo { + public static void main(String[] args) { + MathPlot p = MathPlot.of(Size.of(40, 20)).ranges(-2 * Math.PI, 2 * Math.PI, -1.0, 1.0); + // plot a sine wave + p.plot(Math::sin); + Framed f = Framed.of(p).title(Line.of(" Sine Wave ")); + Panel pnl = Panel.of(42, 22); + f.render(pnl); + System.out.println(pnl); + } +} diff --git a/twinkle-text/pom.xml b/twinkle-core/pom.xml similarity index 88% rename from twinkle-text/pom.xml rename to twinkle-core/pom.xml index 37be25f..e67a51e 100644 --- a/twinkle-text/pom.xml +++ b/twinkle-core/pom.xml @@ -11,10 +11,10 @@ ../pom.xml - twinkle-text + twinkle-core jar - Text manipulation + Core module for Twinkle TUI library diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/decor/SimpleBorderRenderer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/decor/SimpleBorderRenderer.java new file mode 100644 index 0000000..0c47928 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/decor/SimpleBorderRenderer.java @@ -0,0 +1,176 @@ +package org.codejive.twinkle.core.decor; + +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.widget.Renderable; + +public class SimpleBorderRenderer implements Renderable { + private LineStyle leftLineStyle; + private LineStyle rightLineStyle; + private LineStyle topLineStyle; + private LineStyle bottomLineStyle; + private CornerStyle cornerStyle; + private long styleState; + + public enum LineStyle { + SINGLE('─', '│'), + DOUBLE('═', '║'), + ASCII('-', '|'); + + public final char horizontalChar; + public final char verticalChar; + + LineStyle(char horizontalChar, char verticalChar) { + this.horizontalChar = horizontalChar; + this.verticalChar = verticalChar; + } + } + + public enum CornerStyle { + ROUNDED('╭', '╮', '╰', '╯'), + SQUARE('┌', '┐', '└', '┘'), + ASCII('+', '+', '+', '+'); + + public final char topLeftChar; + public final char topRightChar; + public final char bottomLeftChar; + public final char bottomRightChar; + + CornerStyle( + char topLeftChar, char topRightChar, char bottomLeftChar, char bottomRightChar) { + this.topLeftChar = topLeftChar; + this.topRightChar = topRightChar; + this.bottomLeftChar = bottomLeftChar; + this.bottomRightChar = bottomRightChar; + } + } + + public SimpleBorderRenderer() { + this.leftLineStyle = LineStyle.SINGLE; + this.rightLineStyle = LineStyle.SINGLE; + this.topLineStyle = LineStyle.SINGLE; + this.bottomLineStyle = LineStyle.SINGLE; + this.cornerStyle = CornerStyle.ROUNDED; + this.styleState = Style.F_UNSTYLED; + } + + public SimpleBorderRenderer lineStyle(LineStyle lineStyle) { + this.leftLineStyle = lineStyle; + this.rightLineStyle = lineStyle; + this.topLineStyle = lineStyle; + this.bottomLineStyle = lineStyle; + return this; + } + + public SimpleBorderRenderer leftLineStyle(LineStyle lineStyle) { + this.leftLineStyle = lineStyle; + return this; + } + + public SimpleBorderRenderer rightLineStyle(LineStyle lineStyle) { + this.rightLineStyle = lineStyle; + return this; + } + + public SimpleBorderRenderer topLineStyle(LineStyle lineStyle) { + this.topLineStyle = lineStyle; + return this; + } + + public SimpleBorderRenderer bottomLineStyle(LineStyle lineStyle) { + this.bottomLineStyle = lineStyle; + return this; + } + + public SimpleBorderRenderer cornerStyle(CornerStyle cornerStyle) { + this.cornerStyle = cornerStyle; + return this; + } + + public SimpleBorderRenderer style(Style style) { + return style(style.state()); + } + + public SimpleBorderRenderer style(long styleState) { + this.styleState = styleState; + return this; + } + + @Override + public void render(Canvas canvas) { + canvas.setCharAt( + 0, 0, styleState, corner(cornerStyle.topLeftChar, leftLineStyle, topLineStyle)); + canvas.drawHLineAt( + 1, 0, canvas.size().width() - 1, styleState, topLineStyle.horizontalChar); + canvas.setCharAt( + canvas.size().width() - 1, + 0, + styleState, + corner(cornerStyle.topRightChar, rightLineStyle, topLineStyle)); + canvas.drawVLineAt( + 0, 1, canvas.size().height() - 1, styleState, leftLineStyle.verticalChar); + canvas.setCharAt( + 0, + canvas.size().height() - 1, + styleState, + corner(cornerStyle.bottomLeftChar, leftLineStyle, bottomLineStyle)); + canvas.drawHLineAt( + 1, + canvas.size().height() - 1, + canvas.size().width() - 1, + styleState, + bottomLineStyle.horizontalChar); + canvas.setCharAt( + canvas.size().width() - 1, + canvas.size().height() - 1, + styleState, + corner(cornerStyle.bottomRightChar, rightLineStyle, bottomLineStyle)); + canvas.drawVLineAt( + canvas.size().width() - 1, + 1, + canvas.size().height() - 1, + styleState, + rightLineStyle.verticalChar); + } + + private char corner(char corner, LineStyle lineStyle1, LineStyle lineStyle2) { + if (cornerStyle != CornerStyle.SQUARE) { + return corner; + } + if (lineStyle1 == LineStyle.DOUBLE && lineStyle2 == LineStyle.DOUBLE) { + switch (corner) { + case '┌': + return '╔'; + case '┐': + return '╗'; + case '└': + return '╚'; + case '┘': + return '╝'; + } + } else if (lineStyle1 == LineStyle.DOUBLE && lineStyle2 == LineStyle.SINGLE) { + switch (corner) { + case '┌': + return '╓'; + case '┐': + return '╖'; + case '└': + return '╙'; + case '┘': + return '╜'; + } + } else if (lineStyle1 == LineStyle.SINGLE && lineStyle2 == LineStyle.DOUBLE) { + switch (corner) { + case '┌': + return '╒'; + case '┐': + return '╕'; + case '└': + return '╘'; + case '┘': + return '╛'; + } + } + return corner; + } +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Console.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Console.java new file mode 100644 index 0000000..f9c94d0 --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Console.java @@ -0,0 +1,660 @@ +package org.codejive.twinkle.core.text; + +import java.net.URI; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +import org.codejive.twinkle.ansi.Ansi; +import org.codejive.twinkle.ansi.Color; +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.widget.Panel; + +public final class Console { + private Console() {} + + // ---- Fluent builder (Span/Line -> Panel -> ANSI) ---- + + /** Entry point for building rich console output using a style stack. */ + public static Rich text() { + return new Rich(); + } + + public static final class Rich { + private final SpanLinesBuilder out = new SpanLinesBuilder(); + private final Deque