Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2128,10 +2128,7 @@ private static String generateSpanTag(long attr, boolean terminalInverse) {
// Handle concealed
if (conceal) {
fg = bg; // Make text invisible by setting foreground to background
}

// Handle dim (reduce foreground intensity)
if (dim) {
} else if (dim) { // Handle dim (reduce foreground intensity)
fg = (((fg >> 8) & 0x0f) >> 1) << 8 | (((fg >> 4) & 0x0f) >> 1) << 4 | ((fg & 0x0f) >> 1);
}

Expand Down
119 changes: 79 additions & 40 deletions builtins/src/main/java/org/jline/builtins/SwingTerminal.java
Original file line number Diff line number Diff line change
Expand Up @@ -622,40 +622,11 @@ private void paintCell(Graphics2D g2d, int x, int y, long cell, boolean isCursor
return;
}

// Extract colors and attributes
int bg = (int) ((attr) & 0x0fff);
int fg = (int) ((attr >>> 12) & 0x0fff);
// Resolve effective colors (handles defaults, inverse, conceal, dim, cursor)
int[] colors = resolveColors(attr, isCursor, cursorVisible.get());
int fg = colors[0];
int bg = colors[1];
boolean underline = (attr & 0x01000000L) != 0;
boolean inverse = (attr & 0x02000000L) != 0;
boolean conceal = (attr & 0x04000000L) != 0;
boolean bold = (attr & 0x08000000L) != 0;
boolean fgset = (attr & 0x10000000L) != 0;
boolean bgset = (attr & 0x20000000L) != 0;

if (!fgset) {
fg = 0x0fff; // Default white foreground
}
if (!bgset) {
bg = 0; // Default black background
}

// Handle inverse
if (inverse) {
int temp = fg;
fg = bg;
bg = temp;
}

// Handle conceal - hide text by making foreground match background
if (conceal) {
fg = bg;
}

// Handle cursor
if (isCursor && cursorVisible.get()) {
bg = 0x0fff; // White background for cursor
fg = 0; // Black foreground for cursor
}

// Calculate position
int cellX = x * charWidth;
Expand All @@ -671,13 +642,7 @@ private void paintCell(Graphics2D g2d, int x, int y, long cell, boolean isCursor
// Paint character if not space
if (cp != ' ') {
g2d.setColor(getAnsiColor(fg));

// Set font style
Font font = terminalFont;
if (bold) {
font = font.deriveFont(Font.BOLD);
}
g2d.setFont(font);
g2d.setFont(terminalFont.deriveFont(resolveFontStyle(attr)));

// Draw character using code point for proper non-BMP support
String str = new String(Character.toChars(cp));
Expand All @@ -704,6 +669,80 @@ private static boolean isWideCharacter(int cp) {
|| (cp >= 0x30000 && cp <= 0x3FFFD);
}

/**
* Resolves the effective foreground and background colors from cell attributes,
* accounting for default colors, inverse video, concealed text, and dim mode.
*
* @param attr the 32-bit attribute value (upper half of the cell long, already right-shifted)
* @param isCursor true if this cell is at the cursor position
* @param cursorVisible true if the cursor is currently visible
* @return a two-element array: [0] = foreground color (12-bit RGB), [1] = background color (12-bit RGB)
*/
static int[] resolveColors(long attr, boolean isCursor, boolean cursorVisible) {
int bg = (int) ((attr) & 0x0fff);
int fg = (int) ((attr >>> 12) & 0x0fff);
boolean inverse = (attr & 0x02000000L) != 0;
boolean conceal = (attr & 0x04000000L) != 0;
boolean fgset = (attr & 0x10000000L) != 0;
boolean bgset = (attr & 0x20000000L) != 0;
boolean dim = (attr & 0x40000000L) != 0;

if (!fgset) {
fg = 0x0fff; // Default white foreground
}
if (!bgset) {
bg = 0; // Default black background
}

if (inverse) {
int temp = fg;
fg = bg;
bg = temp;
}

if (conceal) {
fg = bg;
} else if (dim) {
fg = dimColor(fg);
}

if (isCursor && cursorVisible) {
bg = 0x0fff;
fg = 0;
}

return new int[] {fg, bg};
}

/**
* Reduces the intensity of a 12-bit RGB color by halving each 4-bit channel.
*
* @param color the 12-bit RGB color value (0x000–0xfff)
* @return the dimmed color with each channel halved
*/
static int dimColor(int color) {
return (((color >> 8) & 0x0f) >> 1) << 8 | (((color >> 4) & 0x0f) >> 1) << 4 | ((color & 0x0f) >> 1);
}

/**
* Determines the AWT font style flags from cell attributes.
*
* @param attr the 32-bit attribute value (upper half of the cell long, already right-shifted)
* @return a combination of {@link Font#PLAIN}, {@link Font#BOLD}, and {@link Font#ITALIC}
*/
static int resolveFontStyle(long attr) {
boolean bold = (attr & 0x08000000L) != 0;
boolean italic = (attr & 0x80000000L) != 0;
int style = Font.PLAIN;
if (bold) {
style |= Font.BOLD;
}
if (italic) {
style |= Font.ITALIC;
}
return style;
}

/**
* Converts a 12-bit packed color value to a {@link Color}.
* <p>
Expand Down
18 changes: 18 additions & 0 deletions builtins/src/test/java/org/jline/builtins/ScreenTerminalTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,24 @@ public void testItalicAttribute() throws InterruptedException {
assertTrue(dump.contains("font-style:italic;"), "Should contain italic style");
}

/**
* When both conceal (SGR 8) and dim (SGR 2) are active, conceal must win:
* the foreground must equal the background so text remains invisible.
* Dim must not alter the foreground after concealment.
*/
@Test
void testConcealPlusDimInteraction() throws InterruptedException {
ScreenTerminal terminal = new ScreenTerminal(80, 24);
// Set dim + conceal, then write a character
terminal.write("\033[2;8mX\033[0m");

String dump = terminal.dump(0, true);
// With conceal active, fg must equal bg regardless of dim.
// Default bg is #000000, so fg should also be #000000 (not a dimmed value).
assertTrue(dump.contains("color:#000000;"), "Concealed+dim: fg should match bg, got: " + dump);
assertTrue(dump.contains("background-color:#000000;"), "Background should be default black, got: " + dump);
}

/**
* SGR 22 (normal intensity) must reset both bold and dim per ECMA-48.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Copyright (c) the original author(s).
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* https://opensource.org/licenses/BSD-3-Clause
*/
package org.jline.builtins;

import java.awt.*;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

/**
* Tests for {@link SwingTerminal.TerminalComponent} rendering helper methods:
* color resolution, dim color computation, and font style resolution.
*/
class SwingTerminalRenderingTest {

// Attribute bit masks (matching ScreenTerminal/SwingTerminal encoding)
private static final long UNDERLINE = 0x01000000L;
private static final long INVERSE = 0x02000000L;
private static final long CONCEAL = 0x04000000L;
private static final long BOLD = 0x08000000L;
private static final long FG_SET = 0x10000000L;
private static final long BG_SET = 0x20000000L;
private static final long DIM = 0x40000000L;
private static final long ITALIC = 0x80000000L;

/**
* Encodes foreground and background 12-bit colors with attribute flags into
* the 32-bit attribute format used after {@code cell >>> 32}.
*/
private static long attr(int fg, int bg, long flags) {
// Lower 12 bits: bg, next 12 bits: fg, upper bits: flags
return (long) bg | ((long) fg << 12) | flags;
}

// -----------------------------------------------------------------------
// dimColor tests
// -----------------------------------------------------------------------

@Test
void testDimColorHalvesEachChannel() {
// White (0xfff): each 4-bit channel 0xf → 0x7
assertEquals(0x777, SwingTerminal.TerminalComponent.dimColor(0xfff));
}

@Test
void testDimColorBlackStaysBlack() {
assertEquals(0x000, SwingTerminal.TerminalComponent.dimColor(0x000));
}

@Test
void testDimColorSingleChannel() {
// Red 0xf00 → 0x700
assertEquals(0x700, SwingTerminal.TerminalComponent.dimColor(0xf00));
// Green 0x0f0 → 0x070
assertEquals(0x070, SwingTerminal.TerminalComponent.dimColor(0x0f0));
// Blue 0x00f → 0x007
assertEquals(0x007, SwingTerminal.TerminalComponent.dimColor(0x00f));
}

// -----------------------------------------------------------------------
// resolveColors tests
// -----------------------------------------------------------------------

@Test
void testResolveColorsDefaults() {
// No flags set → default fg=0xfff (white), bg=0x000 (black)
int[] colors = SwingTerminal.TerminalComponent.resolveColors(0L, false, false);
assertEquals(0x0fff, colors[0], "Default foreground should be white");
assertEquals(0x0000, colors[1], "Default background should be black");
}

@Test
void testResolveColorsExplicitFgBg() {
// fg=0x800 (red-ish), bg=0x008 (blue-ish), both set
long a = attr(0x800, 0x008, FG_SET | BG_SET);
int[] colors = SwingTerminal.TerminalComponent.resolveColors(a, false, false);
assertEquals(0x800, colors[0], "Foreground");
assertEquals(0x008, colors[1], "Background");
}

@Test
void testResolveColorsInverse() {
long a = attr(0xfff, 0x000, FG_SET | BG_SET | INVERSE);
int[] colors = SwingTerminal.TerminalComponent.resolveColors(a, false, false);
assertEquals(0x000, colors[0], "Inverse: fg should be original bg");
assertEquals(0xfff, colors[1], "Inverse: bg should be original fg");
}

@Test
void testResolveColorsConcealHidesForeground() {
long a = attr(0xfff, 0x000, FG_SET | BG_SET | CONCEAL);
int[] colors = SwingTerminal.TerminalComponent.resolveColors(a, false, false);
assertEquals(0x000, colors[0], "Concealed: fg should equal bg");
assertEquals(0x000, colors[1]);
}

@Test
void testResolveColorsDimReducesForeground() {
// Default fg is white (0xfff), dimmed → 0x777
long a = attr(0, 0, DIM);
int[] colors = SwingTerminal.TerminalComponent.resolveColors(a, false, false);
assertEquals(0x777, colors[0], "Dim: fg should be halved");
assertEquals(0x000, colors[1], "Dim: bg unchanged");
}

@Test
void testResolveColorsConcealPlusDim() {
// When both conceal and dim are active, conceal wins — fg must equal bg
long a = attr(0xfff, 0x000, FG_SET | BG_SET | CONCEAL | DIM);
int[] colors = SwingTerminal.TerminalComponent.resolveColors(a, false, false);
assertEquals(colors[1], colors[0], "Conceal+dim: fg must equal bg (conceal wins)");
assertEquals(0x000, colors[0], "Conceal+dim: fg should be bg color, not dimmed");
}

@Test
void testResolveColorsCursorOverridesAll() {
long a = attr(0x800, 0x008, FG_SET | BG_SET | DIM | CONCEAL);
int[] colors = SwingTerminal.TerminalComponent.resolveColors(a, true, true);
assertEquals(0x000, colors[0], "Cursor: fg should be black");
assertEquals(0x0fff, colors[1], "Cursor: bg should be white");
}

@Test
void testResolveColorsCursorNotVisibleNoOverride() {
long a = attr(0x800, 0x008, FG_SET | BG_SET);
int[] colors = SwingTerminal.TerminalComponent.resolveColors(a, true, false);
assertEquals(0x800, colors[0], "Invisible cursor: fg unchanged");
assertEquals(0x008, colors[1], "Invisible cursor: bg unchanged");
}

// -----------------------------------------------------------------------
// resolveFontStyle tests
// -----------------------------------------------------------------------

@Test
void testResolveFontStylePlain() {
assertEquals(Font.PLAIN, SwingTerminal.TerminalComponent.resolveFontStyle(0L));
}

@Test
void testResolveFontStyleBold() {
assertEquals(Font.BOLD, SwingTerminal.TerminalComponent.resolveFontStyle(BOLD));
}

@Test
void testResolveFontStyleItalic() {
assertEquals(Font.ITALIC, SwingTerminal.TerminalComponent.resolveFontStyle(ITALIC));
}

@Test
void testResolveFontStyleBoldItalic() {
assertEquals(Font.BOLD | Font.ITALIC, SwingTerminal.TerminalComponent.resolveFontStyle(BOLD | ITALIC));
}

@Test
void testResolveFontStyleIgnoresOtherFlags() {
// Underline, inverse, etc. should not affect font style
assertEquals(Font.PLAIN, SwingTerminal.TerminalComponent.resolveFontStyle(UNDERLINE | INVERSE | CONCEAL));
}
}