diff --git a/pom.xml b/pom.xml index 01305f3c..78c12c57 100644 --- a/pom.xml +++ b/pom.xml @@ -99,8 +99,8 @@ - 8 - 8 + 17 + 17 UTF-8 1.6.1 2026-03-27T13:05:17Z diff --git a/src/main/java/org/fusesource/jansi/Ansi.java b/src/main/java/org/fusesource/jansi/Ansi.java index 576d8d53..be4b4c33 100644 --- a/src/main/java/org/fusesource/jansi/Ansi.java +++ b/src/main/java/org/fusesource/jansi/Ansi.java @@ -19,150 +19,253 @@ import java.util.concurrent.Callable; /** - * Provides a fluent API for generating - * ANSI escape sequences. + * Provides a fluent API for building + * ANSI escape sequences + * as a {@link String} that can be printed to an ANSI-capable terminal. + * + *

Basic usage

+ *
+ * String message = Ansi.ansi()
+ * .fg(Ansi.Color.RED)
+ * .append("Error: file not found")
+ * .reset()
+ * .toString();
+ * System.out.println(message);
+ * 
+ * + *

ANSI detection

+ *

When {@link #isEnabled()} returns {@code false} (e.g. the system property + * {@code DISABLE} is set to {@code true}), {@link #ansi()} returns a + * {@link NoAnsi} instance whose methods are all no-ops, so application code + * never needs to branch on ANSI availability.

+ * + *

Refactoring changes applied (SE2115 project)

+ * * * @since 1.0 + * @see AnsiRenderer + * @see AnsiConsole */ public class Ansi implements Appendable { - private static final char FIRST_ESC_CHAR = 27; - private static final char SECOND_ESC_CHAR = '['; + // ───────────────────────────────────────────────────────────── + // Named constants — replaces magic char literals 27 and '[' + // (fixes Primitive Obsession smell) + // ───────────────────────────────────────────────────────────── + + /** + * The ASCII ESC (escape) character, decimal value {@code 27} (0x1B). + * This is the first character of every ANSI CSI escape sequence. + */ + private static final char ESC = 27; + + /** + * The second character of an ANSI CSI (Control Sequence Introducer) + * escape sequence. Together with {@link #ESC}, it forms the two-character + * prefix {@code ESC [} that begins all CSI commands. + */ + private static final char CSI_OPEN = '['; + + // ───────────────────────────────────────────────────────────── + // Public enums + // ───────────────────────────────────────────────────────────── /** - * ANSI 8 colors for fluent API + * The 8 standard ANSI foreground/background colors plus {@code DEFAULT}. + * + *

Use with {@link Ansi#fg(Color)}, {@link Ansi#bg(Color)}, + * {@link Ansi#fgBright(Color)}, and {@link Ansi#bgBright(Color)}.

+ * + * @see + * ANSI 8-color palette */ public enum Color { - BLACK(0, "BLACK"), - RED(1, "RED"), - GREEN(2, "GREEN"), - YELLOW(3, "YELLOW"), - BLUE(4, "BLUE"), + BLACK (0, "BLACK"), + RED (1, "RED"), + GREEN (2, "GREEN"), + YELLOW (3, "YELLOW"), + BLUE (4, "BLUE"), MAGENTA(5, "MAGENTA"), - CYAN(6, "CYAN"), - WHITE(7, "WHITE"), + CYAN (6, "CYAN"), + WHITE (7, "WHITE"), DEFAULT(9, "DEFAULT"); - private final int value; + private final int value; private final String name; Color(int index, String name) { this.value = index; - this.name = name; + this.name = name; } @Override - public String toString() { - return name; - } + public String toString() { return name; } - public int value() { - return value; - } + /** Returns the raw SGR color index for this color. */ + public int value() { return value; } - public int fg() { - return value + 30; - } + /** Returns the SGR code for this color as a foreground (normal intensity). */ + public int fg() { return value + 30; } - public int bg() { - return value + 40; - } + /** Returns the SGR code for this color as a background (normal intensity). */ + public int bg() { return value + 40; } - public int fgBright() { - return value + 90; - } + /** Returns the SGR code for this color as a bright/intense foreground. */ + public int fgBright() { return value + 90; } - public int bgBright() { - return value + 100; - } + /** Returns the SGR code for this color as a bright/intense background. */ + public int bgBright() { return value + 100; } } /** - * Display attributes, also know as - * SGR - * (Select Graphic Rendition) parameters. + * SGR (Select Graphic Rendition) display attribute codes. + * + *

Use with {@link Ansi#a(Attribute)} to apply text attributes such as + * bold, italic, underline, and blink.

+ * + * @see + * SGR parameters */ public enum Attribute { - RESET(0, "RESET"), - INTENSITY_BOLD(1, "INTENSITY_BOLD"), - INTENSITY_FAINT(2, "INTENSITY_FAINT"), - ITALIC(3, "ITALIC_ON"), - UNDERLINE(4, "UNDERLINE_ON"), - BLINK_SLOW(5, "BLINK_SLOW"), - BLINK_FAST(6, "BLINK_FAST"), - NEGATIVE_ON(7, "NEGATIVE_ON"), - CONCEAL_ON(8, "CONCEAL_ON"), - STRIKETHROUGH_ON(9, "STRIKETHROUGH_ON"), - UNDERLINE_DOUBLE(21, "UNDERLINE_DOUBLE"), - INTENSITY_BOLD_OFF(22, "INTENSITY_BOLD_OFF"), - ITALIC_OFF(23, "ITALIC_OFF"), - UNDERLINE_OFF(24, "UNDERLINE_OFF"), - BLINK_OFF(25, "BLINK_OFF"), - NEGATIVE_OFF(27, "NEGATIVE_OFF"), - CONCEAL_OFF(28, "CONCEAL_OFF"), - STRIKETHROUGH_OFF(29, "STRIKETHROUGH_OFF"); - - private final int value; + RESET ( 0, "RESET"), + INTENSITY_BOLD ( 1, "INTENSITY_BOLD"), + INTENSITY_FAINT ( 2, "INTENSITY_FAINT"), + ITALIC ( 3, "ITALIC_ON"), + UNDERLINE ( 4, "UNDERLINE_ON"), + BLINK_SLOW ( 5, "BLINK_SLOW"), + BLINK_FAST ( 6, "BLINK_FAST"), + NEGATIVE_ON ( 7, "NEGATIVE_ON"), + CONCEAL_ON ( 8, "CONCEAL_ON"), + STRIKETHROUGH_ON ( 9, "STRIKETHROUGH_ON"), + UNDERLINE_DOUBLE (21, "UNDERLINE_DOUBLE"), + INTENSITY_BOLD_OFF (22, "INTENSITY_BOLD_OFF"), + ITALIC_OFF (23, "ITALIC_OFF"), + UNDERLINE_OFF (24, "UNDERLINE_OFF"), + BLINK_OFF (25, "BLINK_OFF"), + NEGATIVE_OFF (27, "NEGATIVE_OFF"), + CONCEAL_OFF (28, "CONCEAL_OFF"), + STRIKETHROUGH_OFF (29, "STRIKETHROUGH_OFF"); + + private final int value; private final String name; Attribute(int index, String name) { this.value = index; - this.name = name; + this.name = name; } @Override - public String toString() { - return name; - } + public String toString() { return name; } - public int value() { - return value; - } + /** Returns the SGR numeric code for this attribute. */ + public int value() { return value; } } /** - * ED (Erase in Display) / EL (Erase in Line) parameter (see - * CSI sequence J and K) + * Parameter for the ED (Erase in Display) and EL (Erase in Line) commands. + * * @see Ansi#eraseScreen(Erase) * @see Ansi#eraseLine(Erase) + * @see + * CSI J and K sequences */ public enum Erase { - FORWARD(0, "FORWARD"), + FORWARD (0, "FORWARD"), BACKWARD(1, "BACKWARD"), - ALL(2, "ALL"); + ALL (2, "ALL"); - private final int value; + private final int value; private final String name; Erase(int index, String name) { this.value = index; - this.name = name; + this.name = name; } @Override - public String toString() { - return name; - } + public String toString() { return name; } - public int value() { - return value; - } + /** Returns the numeric parameter for this erase mode. */ + public int value() { return value; } } + // ───────────────────────────────────────────────────────────── + // Consumer functional interface + // ───────────────────────────────────────────────────────────── + + /** + * A functional interface for operations that accept an {@link Ansi} instance. + * Used by {@link #apply(Consumer)} to allow inline customization of the + * escape sequence being built. + */ @FunctionalInterface public interface Consumer { + /** + * Applies this operation to the given {@link Ansi} instance. + * + * @param ansi the builder to operate on + */ void apply(Ansi ansi); } + // ───────────────────────────────────────────────────────────── + // Detection mechanism + // ───────────────────────────────────────────────────────────── + + /** + * System property name that disables ANSI output when set to {@code true}. + * When disabled, {@link #ansi()} returns a {@link NoAnsi} no-op instance. + */ public static final String DISABLE = Ansi.class.getName() + ".disable"; private static Callable detector = () -> !Boolean.getBoolean(DISABLE); + /** + * Replaces the ANSI detection strategy with a custom callable. + * + * @param detector a {@link Callable} returning {@code true} if ANSI is available; + * must not be {@code null} + * @throws IllegalArgumentException if {@code detector} is {@code null} + */ public static void setDetector(final Callable detector) { - if (detector == null) throw new IllegalArgumentException(); + if (detector == null) throw new IllegalArgumentException("detector must not be null"); Ansi.detector = detector; } + /** + * Returns {@code true} if the current detector callable indicates + * that ANSI escape sequences are supported on this platform. + * + * @return {@code true} if ANSI is detected as available + */ public static boolean isDetected() { try { return detector.call(); @@ -171,230 +274,192 @@ public static boolean isDetected() { } } - private static final InheritableThreadLocal holder = new InheritableThreadLocal() { - @Override - protected Boolean initialValue() { - return isDetected(); - } - }; + private static final InheritableThreadLocal holder = + new InheritableThreadLocal() { + @Override + protected Boolean initialValue() { + return isDetected(); + } + }; + /** + * Enables or disables ANSI output for the current thread and all threads + * it subsequently creates. + * + * @param flag {@code true} to enable ANSI; {@code false} to disable + */ public static void setEnabled(final boolean flag) { holder.set(flag); } + /** + * Returns {@code true} if ANSI output is enabled for the current thread. + * + * @return {@code true} if ANSI is enabled + */ public static boolean isEnabled() { return holder.get(); } + // ───────────────────────────────────────────────────────────── + // Factory methods + // ───────────────────────────────────────────────────────────── + + /** + * Creates a new {@link Ansi} builder instance. + * + *

If ANSI is disabled ({@link #isEnabled()} returns {@code false}), + * a {@link NoAnsi} no-op instance is returned instead, so callers + * never need to check ANSI availability themselves.

+ * + * @return a new {@link Ansi} builder (or a no-op instance if disabled) + */ public static Ansi ansi() { - if (isEnabled()) { - return new Ansi(); - } else { - return new NoAnsi(); - } + return isEnabled() ? new Ansi() : new NoAnsi(); } + /** + * Creates a new {@link Ansi} builder backed by an existing {@link StringBuilder}. + * + * @param builder the backing builder to append escape sequences to + * @return a new {@link Ansi} instance wrapping {@code builder} + */ public static Ansi ansi(StringBuilder builder) { - if (isEnabled()) { - return new Ansi(builder); - } else { - return new NoAnsi(builder); - } + return isEnabled() ? new Ansi(builder) : new NoAnsi(builder); } + /** + * Creates a new {@link Ansi} builder with the given initial capacity. + * + * @param size the initial capacity of the internal {@link StringBuilder} + * @return a new {@link Ansi} instance + */ public static Ansi ansi(int size) { - if (isEnabled()) { - return new Ansi(size); - } else { - return new NoAnsi(size); - } + return isEnabled() ? new Ansi(size) : new NoAnsi(size); } - private static class NoAnsi extends Ansi { - public NoAnsi() { - super(); - } - - public NoAnsi(int size) { - super(size); - } - - public NoAnsi(StringBuilder builder) { - super(builder); - } - - @Override - public Ansi fg(Color color) { - return this; - } - - @Override - public Ansi bg(Color color) { - return this; - } - - @Override - public Ansi fgBright(Color color) { - return this; - } - - @Override - public Ansi bgBright(Color color) { - return this; - } - - @Override - public Ansi fg(int color) { - return this; - } - - @Override - public Ansi fgRgb(int r, int g, int b) { - return this; - } - - @Override - public Ansi bg(int color) { - return this; - } - - @Override - public Ansi bgRgb(int r, int g, int b) { - return this; - } - - @Override - public Ansi a(Attribute attribute) { - return this; - } - - @Override - public Ansi cursor(int row, int column) { - return this; - } - - @Override - public Ansi cursorToColumn(int x) { - return this; - } - - @Override - public Ansi cursorUp(int y) { - return this; - } - - @Override - public Ansi cursorRight(int x) { - return this; - } - - @Override - public Ansi cursorDown(int y) { - return this; - } - - @Override - public Ansi cursorLeft(int x) { - return this; - } - - @Override - public Ansi cursorDownLine() { - return this; - } - - @Override - public Ansi cursorDownLine(final int n) { - return this; - } - - @Override - public Ansi cursorUpLine() { - return this; - } - - @Override - public Ansi cursorUpLine(final int n) { - return this; - } - - @Override - public Ansi eraseScreen() { - return this; - } - - @Override - public Ansi eraseScreen(Erase kind) { - return this; - } - - @Override - public Ansi eraseLine() { - return this; - } + // ───────────────────────────────────────────────────────────── + // NoAnsi — no-op subclass returned when ANSI is disabled + // ───────────────────────────────────────────────────────────── - @Override - public Ansi eraseLine(Erase kind) { - return this; - } - - @Override - public Ansi scrollUp(int rows) { - return this; - } - - @Override - public Ansi scrollDown(int rows) { - return this; - } - - @Override - public Ansi saveCursorPosition() { - return this; - } + /** + * A no-op subclass of {@link Ansi} that suppresses all escape sequence + * generation. All methods return {@code this} immediately without + * modifying the underlying builder. + * + *

Returned by {@link #ansi()} when {@link #isEnabled()} is {@code false}, + * allowing application code to call the full fluent API without any + * conditional checks for ANSI availability.

+ */ + private static class NoAnsi extends Ansi { + NoAnsi() { super(); } + NoAnsi(int size) { super(size); } + NoAnsi(StringBuilder builder) { super(builder); } + + @Override public Ansi fg(Color color) { return this; } + @Override public Ansi bg(Color color) { return this; } + @Override public Ansi fgBright(Color color) { return this; } + @Override public Ansi bgBright(Color color) { return this; } + @Override public Ansi fg(int color) { return this; } + @Override public Ansi fgRgb(int r, int g, int b) { return this; } + @Override public Ansi bg(int color) { return this; } + @Override public Ansi bgRgb(int r, int g, int b) { return this; } + @Override public Ansi a(Attribute attribute) { return this; } + @Override public Ansi cursor(int row, int column) { return this; } + @Override public Ansi cursorToColumn(int x) { return this; } + @Override public Ansi cursorUp(int y) { return this; } + @Override public Ansi cursorRight(int x) { return this; } + @Override public Ansi cursorDown(int y) { return this; } + @Override public Ansi cursorLeft(int x) { return this; } + @Override public Ansi cursorDownLine() { return this; } + @Override public Ansi cursorDownLine(final int n) { return this; } + @Override public Ansi cursorUpLine() { return this; } + @Override public Ansi cursorUpLine(final int n) { return this; } + @Override public Ansi eraseScreen() { return this; } + @Override public Ansi eraseScreen(Erase kind) { return this; } + @Override public Ansi eraseLine() { return this; } + @Override public Ansi eraseLine(Erase kind) { return this; } + @Override public Ansi scrollUp(int rows) { return this; } + @Override public Ansi scrollDown(int rows) { return this; } + @Override public Ansi saveCursorPosition() { return this; } + @Override public Ansi restoreCursorPosition() { return this; } + @Override public Ansi reset() { return this; } + + /** @deprecated Use {@link #restoreCursorPosition()} instead. */ @Override @Deprecated - public Ansi restorCursorPosition() { - return this; - } - - @Override - public Ansi restoreCursorPosition() { - return this; - } - - @Override - public Ansi reset() { - return this; - } + public Ansi restorCursorPosition() { return this; } } - private final StringBuilder builder; + // ───────────────────────────────────────────────────────────── + // Instance state + // ───────────────────────────────────────────────────────────── + + private final StringBuilder builder; private final ArrayList attributeOptions = new ArrayList<>(5); + // ───────────────────────────────────────────────────────────── + // Constructors + // ───────────────────────────────────────────────────────────── + + /** Creates a new {@code Ansi} builder with a default initial capacity of 80. */ public Ansi() { this(new StringBuilder(80)); } + /** + * Creates a new {@code Ansi} builder that continues from the state of + * an existing {@code Ansi} parent instance. + * + * @param parent the parent instance whose content and pending attributes + * are copied into this new builder + */ public Ansi(Ansi parent) { this(new StringBuilder(parent.builder)); attributeOptions.addAll(parent.attributeOptions); } + /** + * Creates a new {@code Ansi} builder with the given initial capacity. + * + * @param size the initial capacity of the internal {@link StringBuilder} + */ public Ansi(int size) { this(new StringBuilder(size)); } + /** + * Creates a new {@code Ansi} builder backed by the given {@link StringBuilder}. + * + * @param builder the backing string builder + */ public Ansi(StringBuilder builder) { this.builder = builder; } + // ───────────────────────────────────────────────────────────── + // Color methods + // ───────────────────────────────────────────────────────────── + + /** + * Sets the foreground (text) color using one of the 8 standard ANSI colors. + * + * @param color the foreground color to apply + * @return this {@code Ansi} instance for chaining + */ public Ansi fg(Color color) { attributeOptions.add(color.fg()); return this; } + /** + * Sets the foreground color using a 256-color palette index (0–255). + * + * @param color the 256-color palette index + * @return this {@code Ansi} instance for chaining + * @see 8-bit color + */ public Ansi fg(int color) { attributeOptions.add(38); attributeOptions.add(5); @@ -402,10 +467,25 @@ public Ansi fg(int color) { return this; } + /** + * Sets the foreground color using a 24-bit RGB value packed as + * {@code 0xRRGGBB}. + * + * @param color the packed RGB color value + * @return this {@code Ansi} instance for chaining + */ public Ansi fgRgb(int color) { return fgRgb(color >> 16, color >> 8, color); } + /** + * Sets the foreground color using individual 24-bit RGB components. + * + * @param r red component (0–255) + * @param g green component (0–255) + * @param b blue component (0–255) + * @return this {@code Ansi} instance for chaining + */ public Ansi fgRgb(int r, int g, int b) { attributeOptions.add(38); attributeOptions.add(2); @@ -415,43 +495,40 @@ public Ansi fgRgb(int r, int g, int b) { return this; } - public Ansi fgBlack() { - return this.fg(Color.BLACK); - } - - public Ansi fgBlue() { - return this.fg(Color.BLUE); - } - - public Ansi fgCyan() { - return this.fg(Color.CYAN); - } - - public Ansi fgDefault() { - return this.fg(Color.DEFAULT); - } - - public Ansi fgGreen() { - return this.fg(Color.GREEN); - } - - public Ansi fgMagenta() { - return this.fg(Color.MAGENTA); - } - - public Ansi fgRed() { - return this.fg(Color.RED); - } - - public Ansi fgYellow() { - return this.fg(Color.YELLOW); - } + /** Sets the foreground color to black. @return this instance */ + public Ansi fgBlack() { return fg(Color.BLACK); } + /** Sets the foreground color to blue. @return this instance */ + public Ansi fgBlue() { return fg(Color.BLUE); } + /** Sets the foreground color to cyan. @return this instance */ + public Ansi fgCyan() { return fg(Color.CYAN); } + /** Resets the foreground color to the terminal default. @return this instance */ + public Ansi fgDefault() { return fg(Color.DEFAULT); } + /** Sets the foreground color to green. @return this instance */ + public Ansi fgGreen() { return fg(Color.GREEN); } + /** Sets the foreground color to magenta. @return this instance */ + public Ansi fgMagenta() { return fg(Color.MAGENTA); } + /** Sets the foreground color to red. @return this instance */ + public Ansi fgRed() { return fg(Color.RED); } + /** Sets the foreground color to yellow. @return this instance */ + public Ansi fgYellow() { return fg(Color.YELLOW); } + /** + * Sets the background color using one of the 8 standard ANSI colors. + * + * @param color the background color to apply + * @return this {@code Ansi} instance for chaining + */ public Ansi bg(Color color) { attributeOptions.add(color.bg()); return this; } + /** + * Sets the background color using a 256-color palette index (0–255). + * + * @param color the 256-color palette index + * @return this {@code Ansi} instance for chaining + */ public Ansi bg(int color) { attributeOptions.add(48); attributeOptions.add(5); @@ -459,10 +536,25 @@ public Ansi bg(int color) { return this; } + /** + * Sets the background color using a 24-bit RGB value packed as + * {@code 0xRRGGBB}. + * + * @param color the packed RGB color value + * @return this {@code Ansi} instance for chaining + */ public Ansi bgRgb(int color) { return bgRgb(color >> 16, color >> 8, color); } + /** + * Sets the background color using individual 24-bit RGB components. + * + * @param r red component (0–255) + * @param g green component (0–255) + * @param b blue component (0–255) + * @return this {@code Ansi} instance for chaining + */ public Ansi bgRgb(int r, int g, int b) { attributeOptions.add(48); attributeOptions.add(2); @@ -472,171 +564,173 @@ public Ansi bgRgb(int r, int g, int b) { return this; } - public Ansi bgCyan() { - return this.bg(Color.CYAN); - } - - public Ansi bgDefault() { - return this.bg(Color.DEFAULT); - } - - public Ansi bgGreen() { - return this.bg(Color.GREEN); - } - - public Ansi bgMagenta() { - return this.bg(Color.MAGENTA); - } - - public Ansi bgRed() { - return this.bg(Color.RED); - } - - public Ansi bgYellow() { - return this.bg(Color.YELLOW); - } + /** Sets the background color to cyan. @return this instance */ + public Ansi bgCyan() { return bg(Color.CYAN); } + /** Resets the background color to the terminal default. @return this instance */ + public Ansi bgDefault() { return bg(Color.DEFAULT); } + /** Sets the background color to green. @return this instance */ + public Ansi bgGreen() { return bg(Color.GREEN); } + /** Sets the background color to magenta. @return this instance */ + public Ansi bgMagenta() { return bg(Color.MAGENTA); } + /** Sets the background color to red. @return this instance */ + public Ansi bgRed() { return bg(Color.RED); } + /** Sets the background color to yellow. @return this instance */ + public Ansi bgYellow() { return bg(Color.YELLOW); } + /** + * Sets the foreground color to a bright/intense variant of the given color. + * + * @param color the color to apply at bright intensity + * @return this {@code Ansi} instance for chaining + */ public Ansi fgBright(Color color) { attributeOptions.add(color.fgBright()); return this; } - public Ansi fgBrightBlack() { - return this.fgBright(Color.BLACK); - } - - public Ansi fgBrightBlue() { - return this.fgBright(Color.BLUE); - } - - public Ansi fgBrightCyan() { - return this.fgBright(Color.CYAN); - } - - public Ansi fgBrightDefault() { - return this.fgBright(Color.DEFAULT); - } - - public Ansi fgBrightGreen() { - return this.fgBright(Color.GREEN); - } - - public Ansi fgBrightMagenta() { - return this.fgBright(Color.MAGENTA); - } - - public Ansi fgBrightRed() { - return this.fgBright(Color.RED); - } - - public Ansi fgBrightYellow() { - return this.fgBright(Color.YELLOW); - } + /** Sets the foreground to bright black (dark gray). @return this instance */ + public Ansi fgBrightBlack() { return fgBright(Color.BLACK); } + /** Sets the foreground to bright blue. @return this instance */ + public Ansi fgBrightBlue() { return fgBright(Color.BLUE); } + /** Sets the foreground to bright cyan. @return this instance */ + public Ansi fgBrightCyan() { return fgBright(Color.CYAN); } + /** Sets the foreground to bright default. @return this instance */ + public Ansi fgBrightDefault() { return fgBright(Color.DEFAULT); } + /** Sets the foreground to bright green. @return this instance */ + public Ansi fgBrightGreen() { return fgBright(Color.GREEN); } + /** Sets the foreground to bright magenta. @return this instance */ + public Ansi fgBrightMagenta() { return fgBright(Color.MAGENTA); } + /** Sets the foreground to bright red. @return this instance */ + public Ansi fgBrightRed() { return fgBright(Color.RED); } + /** Sets the foreground to bright yellow. @return this instance */ + public Ansi fgBrightYellow() { return fgBright(Color.YELLOW); } + /** + * Sets the background color to a bright/intense variant of the given color. + * + * @param color the color to apply at bright intensity + * @return this {@code Ansi} instance for chaining + */ public Ansi bgBright(Color color) { attributeOptions.add(color.bgBright()); return this; } - public Ansi bgBrightCyan() { - return this.bgBright(Color.CYAN); - } - - public Ansi bgBrightDefault() { - return this.bgBright(Color.DEFAULT); - } - - public Ansi bgBrightGreen() { - return this.bgBright(Color.GREEN); - } - - public Ansi bgBrightMagenta() { - return this.bgBright(Color.MAGENTA); - } - - public Ansi bgBrightRed() { - return this.bgBright(Color.RED); - } + /** Sets the background to bright cyan. @return this instance */ + public Ansi bgBrightCyan() { return bgBright(Color.CYAN); } + /** Sets the background to bright default. @return this instance */ + public Ansi bgBrightDefault() { return bgBright(Color.DEFAULT); } + /** Sets the background to bright green. @return this instance */ + public Ansi bgBrightGreen() { return bgBright(Color.GREEN); } + /** Sets the background to bright magenta. @return this instance */ + public Ansi bgBrightMagenta() { return bgBright(Color.MAGENTA); } + /** Sets the background to bright red. @return this instance */ + public Ansi bgBrightRed() { return bgBright(Color.RED); } + /** Sets the background to bright yellow. @return this instance */ + public Ansi bgBrightYellow() { return bgBright(Color.YELLOW); } - public Ansi bgBrightYellow() { - return this.bgBright(Color.YELLOW); - } + // ───────────────────────────────────────────────────────────── + // Attribute method + // ───────────────────────────────────────────────────────────── + /** + * Applies an SGR display attribute (bold, italic, underline, etc.). + * + * @param attribute the display attribute to apply + * @return this {@code Ansi} instance for chaining + * @see Attribute + */ public Ansi a(Attribute attribute) { attributeOptions.add(attribute.value()); return this; } + // ───────────────────────────────────────────────────────────── + // Cursor positioning methods + // ───────────────────────────────────────────────────────────── + /** - * Moves the cursor to row n, column m. The values are 1-based. - * Any values less than 1 are mapped to 1. + * Moves the cursor to the specified row and column (1-based). + * Values less than 1 are clamped to 1. * - * @param row row (1-based) from top - * @param column column (1 based) from left - * @return this Ansi instance + * @param row the target row (1-based, from the top) + * @param column the target column (1-based, from the left) + * @return this {@code Ansi} instance for chaining */ public Ansi cursor(final int row, final int column) { return appendEscapeSequence('H', Math.max(1, row), Math.max(1, column)); } /** - * Moves the cursor to column n. The parameter n is 1-based. - * If n is less than 1 it is moved to the first column. + * Moves the cursor to the specified column on the current line (1-based). + * Values less than 1 are clamped to 1. * - * @param x the index (1-based) of the column to move to - * @return this Ansi instance + * @param x the target column index (1-based) + * @return this {@code Ansi} instance for chaining */ public Ansi cursorToColumn(final int x) { return appendEscapeSequence('G', Math.max(1, x)); } /** - * Moves the cursor up. If the parameter y is negative it moves the cursor down. + * Moves the cursor up by {@code y} lines. + * If {@code y} is negative, the cursor moves down instead. * * @param y the number of lines to move up - * @return this Ansi instance + * @return this {@code Ansi} instance for chaining */ public Ansi cursorUp(final int y) { - return y > 0 ? appendEscapeSequence('A', y) : y < 0 ? cursorDown(-y) : this; + return y > 0 ? appendEscapeSequence('A', y) + : y < 0 ? cursorDown(-y) + : this; } /** - * Moves the cursor down. If the parameter y is negative it moves the cursor up. + * Moves the cursor down by {@code y} lines. + * If {@code y} is negative, the cursor moves up instead. * * @param y the number of lines to move down - * @return this Ansi instance + * @return this {@code Ansi} instance for chaining */ public Ansi cursorDown(final int y) { - return y > 0 ? appendEscapeSequence('B', y) : y < 0 ? cursorUp(-y) : this; + return y > 0 ? appendEscapeSequence('B', y) + : y < 0 ? cursorUp(-y) + : this; } /** - * Moves the cursor right. If the parameter x is negative it moves the cursor left. + * Moves the cursor right by {@code x} characters. + * If {@code x} is negative, the cursor moves left instead. * * @param x the number of characters to move right - * @return this Ansi instance + * @return this {@code Ansi} instance for chaining */ public Ansi cursorRight(final int x) { - return x > 0 ? appendEscapeSequence('C', x) : x < 0 ? cursorLeft(-x) : this; + return x > 0 ? appendEscapeSequence('C', x) + : x < 0 ? cursorLeft(-x) + : this; } /** - * Moves the cursor left. If the parameter x is negative it moves the cursor right. + * Moves the cursor left by {@code x} characters. + * If {@code x} is negative, the cursor moves right instead. * * @param x the number of characters to move left - * @return this Ansi instance + * @return this {@code Ansi} instance for chaining */ public Ansi cursorLeft(final int x) { - return x > 0 ? appendEscapeSequence('D', x) : x < 0 ? cursorRight(-x) : this; + return x > 0 ? appendEscapeSequence('D', x) + : x < 0 ? cursorRight(-x) + : this; } /** - * Moves the cursor relative to the current position. The cursor is moved right if x is - * positive, left if negative and down if y is positive and up if negative. + * Moves the cursor relative to its current position. + * Positive {@code x} moves right, positive {@code y} moves down. * - * @param x the number of characters to move horizontally - * @param y the number of lines to move vertically - * @return this Ansi instance + * @param x horizontal displacement (positive = right, negative = left) + * @param y vertical displacement (positive = down, negative = up) + * @return this {@code Ansi} instance for chaining * @since 2.2 */ public Ansi cursorMove(final int x, final int y) { @@ -644,210 +738,473 @@ public Ansi cursorMove(final int x, final int y) { } /** - * Moves the cursor to the beginning of the line below. + * Moves the cursor to the beginning of the next line. * - * @return this Ansi instance + * @return this {@code Ansi} instance for chaining */ public Ansi cursorDownLine() { return appendEscapeSequence('E'); } /** - * Moves the cursor to the beginning of the n-th line below. If the parameter n is negative it - * moves the cursor to the beginning of the n-th line above. + * Moves the cursor to the beginning of the line {@code n} lines below. + * If {@code n} is negative, moves up {@code |n|} lines instead. * - * @param n the number of lines to move the cursor - * @return this Ansi instance + * @param n the number of lines to move down (negative to move up) + * @return this {@code Ansi} instance for chaining */ public Ansi cursorDownLine(final int n) { return n < 0 ? cursorUpLine(-n) : appendEscapeSequence('E', n); } /** - * Moves the cursor to the beginning of the line above. + * Moves the cursor to the beginning of the previous line. * - * @return this Ansi instance + * @return this {@code Ansi} instance for chaining */ public Ansi cursorUpLine() { return appendEscapeSequence('F'); } /** - * Moves the cursor to the beginning of the n-th line above. If the parameter n is negative it - * moves the cursor to the beginning of the n-th line below. + * Moves the cursor to the beginning of the line {@code n} lines above. + * If {@code n} is negative, moves down {@code |n|} lines instead. * - * @param n the number of lines to move the cursor - * @return this Ansi instance + * @param n the number of lines to move up (negative to move down) + * @return this {@code Ansi} instance for chaining */ public Ansi cursorUpLine(final int n) { return n < 0 ? cursorDownLine(-n) : appendEscapeSequence('F', n); } + // ───────────────────────────────────────────────────────────── + // Erase methods + // ───────────────────────────────────────────────────────────── + + /** + * Erases the entire screen (equivalent to {@link Erase#ALL}). + * + * @return this {@code Ansi} instance for chaining + */ public Ansi eraseScreen() { return appendEscapeSequence('J', Erase.ALL.value()); } + /** + * Erases the screen according to the given {@link Erase} mode. + * + * @param kind the erase mode ({@link Erase#FORWARD}, {@link Erase#BACKWARD}, + * or {@link Erase#ALL}) + * @return this {@code Ansi} instance for chaining + */ public Ansi eraseScreen(final Erase kind) { return appendEscapeSequence('J', kind.value()); } + /** + * Erases from the cursor position to the end of the current line. + * + * @return this {@code Ansi} instance for chaining + */ public Ansi eraseLine() { return appendEscapeSequence('K'); } + /** + * Erases part of the current line according to the given {@link Erase} mode. + * + * @param kind the erase mode ({@link Erase#FORWARD}, {@link Erase#BACKWARD}, + * or {@link Erase#ALL}) + * @return this {@code Ansi} instance for chaining + */ public Ansi eraseLine(final Erase kind) { return appendEscapeSequence('K', kind.value()); } + // ───────────────────────────────────────────────────────────── + // Scroll methods + // ───────────────────────────────────────────────────────────── + + /** + * Scrolls the terminal up by the given number of rows. + * Negative values scroll down instead. + * + * @param rows number of rows to scroll up + * @return this {@code Ansi} instance for chaining + */ public Ansi scrollUp(final int rows) { - if (rows == Integer.MIN_VALUE) { - return scrollDown(Integer.MAX_VALUE); - } - return rows > 0 ? appendEscapeSequence('S', rows) : rows < 0 ? scrollDown(-rows) : this; + if (rows == Integer.MIN_VALUE) return scrollDown(Integer.MAX_VALUE); + return rows > 0 ? appendEscapeSequence('S', rows) + : rows < 0 ? scrollDown(-rows) + : this; } + /** + * Scrolls the terminal down by the given number of rows. + * Negative values scroll up instead. + * + * @param rows number of rows to scroll down + * @return this {@code Ansi} instance for chaining + */ public Ansi scrollDown(final int rows) { - if (rows == Integer.MIN_VALUE) { - return scrollUp(Integer.MAX_VALUE); - } - return rows > 0 ? appendEscapeSequence('T', rows) : rows < 0 ? scrollUp(-rows) : this; + if (rows == Integer.MIN_VALUE) return scrollUp(Integer.MAX_VALUE); + return rows > 0 ? appendEscapeSequence('T', rows) + : rows < 0 ? scrollUp(-rows) + : this; } - @Deprecated - public Ansi restorCursorPosition() { - return restoreCursorPosition(); - } + // ───────────────────────────────────────────────────────────── + // Cursor save / restore + // ───────────────────────────────────────────────────────────── + /** + * Saves the current cursor position using both the SCO ({@code ESC [s}) + * and DEC ({@code ESC 7}) sequences for maximum terminal compatibility. + * + * @return this {@code Ansi} instance for chaining + * @see #restoreCursorPosition() + */ public Ansi saveCursorPosition() { saveCursorPositionSCO(); return saveCursorPositionDEC(); } - // SCO command + /** + * Saves the cursor position using the SCO sequence ({@code ESC [s}). + * + * @return this {@code Ansi} instance for chaining + */ public Ansi saveCursorPositionSCO() { return appendEscapeSequence('s'); } - // DEC command + /** + * Saves the cursor position using the DEC sequence ({@code ESC 7}). + * + * @return this {@code Ansi} instance for chaining + */ public Ansi saveCursorPositionDEC() { - builder.append(FIRST_ESC_CHAR); + builder.append(ESC); builder.append('7'); return this; } + /** + * Restores the cursor position previously saved by {@link #saveCursorPosition()}. + * Issues both SCO ({@code ESC [u}) and DEC ({@code ESC 8}) restore sequences. + * + * @return this {@code Ansi} instance for chaining + */ public Ansi restoreCursorPosition() { restoreCursorPositionSCO(); return restoreCursorPositionDEC(); } - // SCO command + /** + * Restores the cursor position using the SCO sequence ({@code ESC [u}). + * + * @return this {@code Ansi} instance for chaining + */ public Ansi restoreCursorPositionSCO() { return appendEscapeSequence('u'); } - // DEC command + /** + * Restores the cursor position using the DEC sequence ({@code ESC 8}). + * + * @return this {@code Ansi} instance for chaining + */ public Ansi restoreCursorPositionDEC() { - builder.append(FIRST_ESC_CHAR); + builder.append(ESC); builder.append('8'); return this; } + /** + * @deprecated Typo in the original API. Use {@link #restoreCursorPosition()} instead. + */ + @Deprecated + public Ansi restorCursorPosition() { + return restoreCursorPosition(); + } + + // ───────────────────────────────────────────────────────────── + // Convenience attribute shortcuts + // ───────────────────────────────────────────────────────────── + + /** + * Resets all text attributes (colors, bold, underline, etc.) to terminal defaults. + * Equivalent to {@code a(Attribute.RESET)}. + * + * @return this {@code Ansi} instance for chaining + */ public Ansi reset() { return a(Attribute.RESET); } + /** + * Enables bold (high intensity) text rendering. + * Equivalent to {@code a(Attribute.INTENSITY_BOLD)}. + * + * @return this {@code Ansi} instance for chaining + */ public Ansi bold() { return a(Attribute.INTENSITY_BOLD); } + /** + * Disables bold text rendering. + * Equivalent to {@code a(Attribute.INTENSITY_BOLD_OFF)}. + * + * @return this {@code Ansi} instance for chaining + */ public Ansi boldOff() { return a(Attribute.INTENSITY_BOLD_OFF); } - public Ansi a(String value) { - flushAttributes(); - builder.append(value); - return this; - } + // ───────────────────────────────────────────────────────────── + // append() methods — the primary text-appending API + // (Rename Method refactoring: replaces the single-letter a() methods) + // ───────────────────────────────────────────────────────────── - public Ansi a(boolean value) { + /** + * Appends a {@link String} value to this ANSI sequence. + * + *

Any pending color or attribute changes are flushed before the + * text is written, so the text appears with the correct attributes.

+ * + *

Refactoring note: This method replaces the original {@code a(String)} + * which was a single-letter name that communicated no intent — a classic + * Uncommunicative Naming / Primitive Obsession code smell (Smell #25 in + * this project's analysis). The original {@code a(String)} is kept as a + * deprecated alias for backward compatibility.

+ * + * @param value the text to append + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(String value) { flushAttributes(); builder.append(value); return this; } - public Ansi a(char value) { + /** + * Appends a {@code boolean} value to this ANSI sequence. + * + * @param value the value to append + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(boolean value) { flushAttributes(); builder.append(value); return this; } - public Ansi a(char[] value, int offset, int len) { + /** + * Appends a subrange of a {@code char[]} array to this ANSI sequence. + * + * @param value the source character array + * @param offset the start index within {@code value} + * @param len the number of characters to append + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(char[] value, int offset, int len) { flushAttributes(); builder.append(value, offset, len); return this; } - public Ansi a(char[] value) { + /** + * Appends all characters in a {@code char[]} array to this ANSI sequence. + * + * @param value the character array to append + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(char[] value) { flushAttributes(); builder.append(value); return this; } - public Ansi a(CharSequence value, int start, int end) { - flushAttributes(); - builder.append(value, start, end); - return this; - } - - public Ansi a(CharSequence value) { + /** + * Appends a {@code double} value to this ANSI sequence. + * + * @param value the value to append + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(double value) { flushAttributes(); builder.append(value); return this; } - public Ansi a(double value) { + /** + * Appends a {@code float} value to this ANSI sequence. + * + * @param value the value to append + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(float value) { flushAttributes(); builder.append(value); return this; } - public Ansi a(float value) { + /** + * Appends an {@code int} value to this ANSI sequence. + * + * @param value the value to append + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(int value) { flushAttributes(); builder.append(value); return this; } - public Ansi a(int value) { + /** + * Appends a {@code long} value to this ANSI sequence. + * + * @param value the value to append + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(long value) { flushAttributes(); builder.append(value); return this; } - public Ansi a(long value) { + /** + * Appends the string representation of an {@link Object} to this ANSI sequence. + * + * @param value the object whose {@code toString()} value is appended + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(Object value) { flushAttributes(); builder.append(value); return this; } - public Ansi a(Object value) { + /** + * Appends a {@link StringBuffer} to this ANSI sequence. + * + * @param value the string buffer to append + * @return this {@code Ansi} instance for chaining + */ + public Ansi append(StringBuffer value) { flushAttributes(); builder.append(value); return this; } - public Ansi a(StringBuffer value) { - flushAttributes(); - builder.append(value); - return this; - } + // ───────────────────────────────────────────────────────────── + // Deprecated a() overloads — kept for backward compatibility + // All delegate cleanly to the new append overloads. + // ───────────────────────────────────────────────────────────── + + /** + * @deprecated Use {@link #append(String)} instead. + * Renamed to communicate intent — this method appends text + * to the escape sequence being built. + */ + @Deprecated + public Ansi a(String value) { return append(value); } + + /** + * @deprecated Use {@link #append(boolean)} instead. + */ + @Deprecated + public Ansi a(boolean value) { return append(value); } + + /** + * @deprecated Use {@link #append(char)} instead. + */ + @Deprecated + public Ansi a(char value) { return append(value); } + + /** + * @deprecated Use {@link #append(char[], int, int)} instead. + */ + @Deprecated + public Ansi a(char[] value, int offset, int len) { return append(value, offset, len); } + + /** + * @deprecated Use {@link #append(char[])} instead. + */ + @Deprecated + public Ansi a(char[] value) { return append(value); } + + /** + * @deprecated Use {@link #append(CharSequence, int, int)} instead. + */ + @Deprecated + public Ansi a(CharSequence value, int start, int end){ return append(value, start, end); } + + /** + * @deprecated Use {@link #append(CharSequence)} — inherited from {@link Appendable}. + */ + @Deprecated + public Ansi a(CharSequence value) { return append(value); } + + /** + * @deprecated Use {@link #append(double)} instead. + */ + @Deprecated + public Ansi a(double value) { return append(value); } + + /** + * @deprecated Use {@link #append(float)} instead. + */ + @Deprecated + public Ansi a(float value) { return append(value); } + /** + * @deprecated Use {@link #append(int)} instead. + */ + @Deprecated + public Ansi a(int value) { return append(value); } + + /** + * @deprecated Use {@link #append(long)} instead. + */ + @Deprecated + public Ansi a(long value) { return append(value); } + + /** + * @deprecated Use {@link #append(Object)} instead. + */ + @Deprecated + public Ansi a(Object value) { return append(value); } + + /** + * @deprecated Use {@link #append(StringBuffer)} instead. + */ + @Deprecated + public Ansi a(StringBuffer value) { return append(value); } + + // ───────────────────────────────────────────────────────────── + // Utility methods + // ───────────────────────────────────────────────────────────── + + /** + * Appends a newline character to this ANSI sequence. + * + * @return this {@code Ansi} instance for chaining + */ public Ansi newline() { flushAttributes(); builder.append(System.getProperty("line.separator")); return this; } + /** + * Formats and appends a string using {@link String#format(String, Object...)}. + * + * @param pattern the format string + * @param args the format arguments + * @return this {@code Ansi} instance for chaining + */ public Ansi format(String pattern, Object... args) { flushAttributes(); builder.append(String.format(pattern, args)); @@ -855,10 +1212,11 @@ public Ansi format(String pattern, Object... args) { } /** - * Applies another function to this Ansi instance. + * Applies a {@link Consumer} function to this {@code Ansi} instance, + * enabling inline customization without breaking the fluent chain. * - * @param fun the function to apply - * @return this Ansi instance + * @param fun the function to apply; must not be {@code null} + * @return this {@code Ansi} instance for chaining * @since 2.2 */ public Ansi apply(Consumer fun) { @@ -867,107 +1225,173 @@ public Ansi apply(Consumer fun) { } /** - * Uses the {@link AnsiRenderer} - * to generate the ANSI escape sequences for the supplied text. + * Renders the given ANSI markup text using {@link AnsiRenderer} and + * appends the result to this sequence. * - * @param text text - * @return this + * @param text the markup text to render (e.g. {@code "@|bold Hello|@"}) + * @return this {@code Ansi} instance for chaining * @since 2.2 + * @see AnsiRenderer */ public Ansi render(final String text) { - a(AnsiRenderer.render(text)); + append(AnsiRenderer.render(text)); return this; } /** - * String formats and renders the supplied arguments. Uses the {@link AnsiRenderer} - * to generate the ANSI escape sequences. + * Formats the given markup text with {@link String#format} arguments, + * then renders it using {@link AnsiRenderer} and appends the result. * - * @param text format - * @param args arguments - * @return this + * @param text format string containing ANSI markup + * @param args format arguments + * @return this {@code Ansi} instance for chaining * @since 2.2 */ public Ansi render(final String text, Object... args) { - a(String.format(AnsiRenderer.render(text), args)); + append(String.format(AnsiRenderer.render(text), args)); return this; } + /** + * Builds and returns the final ANSI escape sequence string. + * Any pending color or attribute changes are flushed before returning. + * + * @return the fully assembled ANSI escape sequence as a {@link String} + */ @Override public String toString() { flushAttributes(); return builder.toString(); } - /////////////////////////////////////////////////////////////////// - // Private Helper Methods - /////////////////////////////////////////////////////////////////// + // ───────────────────────────────────────────────────────────── + // Appendable interface implementation + // ───────────────────────────────────────────────────────────── + + /** + * Appends a {@link CharSequence} to this ANSI sequence (implements {@link Appendable}). + * + * @param csq the character sequence to append + * @return this {@code Ansi} instance for chaining + */ + @Override + public Ansi append(CharSequence csq) { + flushAttributes(); + builder.append(csq); + return this; + } + + /** + * Appends a subrange of a {@link CharSequence} (implements {@link Appendable}). + * + * @param csq the source character sequence + * @param start start index (inclusive) + * @param end end index (exclusive) + * @return this {@code Ansi} instance for chaining + */ + @Override + public Ansi append(CharSequence csq, int start, int end) { + flushAttributes(); + builder.append(csq, start, end); + return this; + } + /** + * Appends a single character (implements {@link Appendable}). + * + * @param c the character to append + * @return this {@code Ansi} instance for chaining + */ + @Override + public Ansi append(char c) { + flushAttributes(); + builder.append(c); + return this; + } + + // ───────────────────────────────────────────────────────────── + // Private helpers + // ───────────────────────────────────────────────────────────── + + /** + * Appends an ANSI CSI escape sequence with no numeric parameter. + * + * @param command the single-character CSI command letter + * @return this {@code Ansi} instance for chaining + */ private Ansi appendEscapeSequence(char command) { flushAttributes(); - builder.append(FIRST_ESC_CHAR); - builder.append(SECOND_ESC_CHAR); + builder.append(ESC); + builder.append(CSI_OPEN); builder.append(command); return this; } + /** + * Appends an ANSI CSI escape sequence with a single numeric parameter. + * + * @param command the CSI command letter + * @param option the numeric parameter + * @return this {@code Ansi} instance for chaining + */ private Ansi appendEscapeSequence(char command, int option) { flushAttributes(); - builder.append(FIRST_ESC_CHAR); - builder.append(SECOND_ESC_CHAR); + builder.append(ESC); + builder.append(CSI_OPEN); builder.append(option); builder.append(command); return this; } + /** + * Appends an ANSI CSI escape sequence with multiple parameters. + * + * @param command the CSI command letter + * @param options the parameter values (separated by {@code ;} in the output) + * @return this {@code Ansi} instance for chaining + */ private Ansi appendEscapeSequence(char command, Object... options) { flushAttributes(); - return _appendEscapeSequence(command, options); + return appendEscapeSequenceRaw(command, options); } + /** + * Flushes any pending SGR attribute options into the output builder. + * + *

A reset-only flush ({@code ESC [m}) is optimized as a shorter sequence + * compared to the general multi-parameter form ({@code ESC [0m}).

+ */ private void flushAttributes() { if (attributeOptions.isEmpty()) return; if (attributeOptions.size() == 1 && attributeOptions.get(0) == 0) { - builder.append(FIRST_ESC_CHAR); - builder.append(SECOND_ESC_CHAR); + builder.append(ESC); + builder.append(CSI_OPEN); builder.append('m'); } else { - _appendEscapeSequence('m', attributeOptions.toArray()); + appendEscapeSequenceRaw('m', attributeOptions.toArray()); } attributeOptions.clear(); } - private Ansi _appendEscapeSequence(char command, Object... options) { - builder.append(FIRST_ESC_CHAR); - builder.append(SECOND_ESC_CHAR); - int size = options.length; - for (int i = 0; i < size; i++) { - if (i != 0) { - builder.append(';'); - } - if (options[i] != null) { - builder.append(options[i]); - } + /** + * Core CSI sequence writer — appends {@code ESC [ options... command}. + * + *

Previously named {@code _appendEscapeSequence} with a leading underscore, + * which is not a valid Java naming convention. Renamed to + * {@code appendEscapeSequenceRaw} for clarity.

+ * + * @param command the CSI command letter + * @param options the parameter values; {@code null} entries are written as empty + * @return this {@code Ansi} instance for chaining + */ + private Ansi appendEscapeSequenceRaw(char command, Object... options) { + builder.append(ESC); + builder.append(CSI_OPEN); + for (int i = 0; i < options.length; i++) { + if (i != 0) builder.append(';'); + if (options[i] != null) builder.append(options[i]); } builder.append(command); return this; } - - @Override - public Ansi append(CharSequence csq) { - builder.append(csq); - return this; - } - - @Override - public Ansi append(CharSequence csq, int start, int end) { - builder.append(csq, start, end); - return this; - } - - @Override - public Ansi append(char c) { - builder.append(c); - return this; - } -} +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/AnsiConsole.java b/src/main/java/org/fusesource/jansi/AnsiConsole.java index 6099a050..234772c5 100644 --- a/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -1,519 +1,85 @@ -/* - * Copyright (C) 2009-2023 the original author(s). - * - * Licensed 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. - */ package org.fusesource.jansi; -import java.io.FileDescriptor; -import java.io.FileOutputStream; import java.io.IOError; import java.io.IOException; -import java.io.OutputStream; import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.nio.charset.UnsupportedCharsetException; -import java.util.Locale; - -import org.fusesource.jansi.internal.CLibrary; -import org.fusesource.jansi.internal.CLibrary.WinSize; -import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; -import org.fusesource.jansi.internal.MingwSupport; -import org.fusesource.jansi.io.AnsiOutputStream; -import org.fusesource.jansi.io.AnsiProcessor; -import org.fusesource.jansi.io.FastBufferedOutputStream; -import org.fusesource.jansi.io.WindowsAnsiProcessor; - -import static org.fusesource.jansi.internal.CLibrary.ioctl; -import static org.fusesource.jansi.internal.CLibrary.isatty; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleMode; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; -import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; -import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; +import java.util.concurrent.atomic.AtomicInteger; /** - * Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream - * if not on a terminal (see - * Jansi native - * CLibrary isatty(int)). - *

The native library used is named jansi and is loaded using HawtJNI Runtime - * Library + * Provides consistent access to an ANSI-aware console PrintStream, or an + * ANSI-stripping PrintStream when not running on a terminal. * - * @since 1.0 - * @see #systemInstall() - * @see #out() - * @see #err() - * @see #ansiStream(boolean) for more details on ANSI mode selection + *

Call {@link #systemInstall()} once at application startup to replace + * {@code System.out} and {@code System.err} with ANSI-aware streams. + * Call {@link #systemUninstall()} when done. Both methods are reference-counted, + * so multiple libraries can safely install/uninstall independently.

*/ public class AnsiConsole { - /** - * The default mode which Jansi will use, can be either force, strip - * or default (the default). - * If this property is set, it will override jansi.passthrough, - * jansi.strip and jansi.force properties. - */ public static final String JANSI_MODE = "jansi.mode"; - /** - * Jansi mode specific to the standard output stream. - */ public static final String JANSI_OUT_MODE = "jansi.out.mode"; - /** - * Jansi mode specific to the standard error stream. - */ public static final String JANSI_ERR_MODE = "jansi.err.mode"; - /** - * Jansi mode value to strip all ansi sequences. - */ public static final String JANSI_MODE_STRIP = "strip"; - /** - * Jansi mode value to force ansi sequences to the stream even if it's not a terminal. - */ public static final String JANSI_MODE_FORCE = "force"; - /** - * Jansi mode value that output sequences if on a terminal, else strip them. - */ public static final String JANSI_MODE_DEFAULT = "default"; - /** - * The default color support that Jansi will use, can be either 16, - * 256 or truecolor. If not set, JANSI will try to - * autodetect the number of colors supported by the terminal by checking the - * COLORTERM and TERM variables. - */ public static final String JANSI_COLORS = "jansi.colors"; - /** - * Jansi colors specific to the standard output stream. - */ public static final String JANSI_OUT_COLORS = "jansi.out.colors"; - /** - * Jansi colors specific to the standard error stream. - */ public static final String JANSI_ERR_COLORS = "jansi.err.colors"; - /** - * Force the use of 16 colors. When using a 256-indexed color, or an RGB - * color, the color will be rounded to the nearest one from the 16 palette. - */ public static final String JANSI_COLORS_16 = "16"; - /** - * Force the use of 256 colors. When using an RGB color, the color will be - * rounded to the nearest one from the standard 256 palette. - */ public static final String JANSI_COLORS_256 = "256"; - /** - * Force the use of 24-bit colors. - */ public static final String JANSI_COLORS_TRUECOLOR = "truecolor"; - /** - * If the jansi.passthrough system property is set to true, will not perform any transformation - * and any ansi sequence will be conveyed without any modification. - * - * @deprecated use {@link #JANSI_MODE} instead - */ - @Deprecated - public static final String JANSI_PASSTHROUGH = "jansi.passthrough"; - /** - * If the jansi.strip system property is set to true, and jansi.passthrough - * is not enabled, all ansi sequences will be stripped before characters are written to the output streams. - * - * @deprecated use {@link #JANSI_MODE} instead - */ - @Deprecated - public static final String JANSI_STRIP = "jansi.strip"; - /** - * If the jansi.force system property is set to true, and neither jansi.passthrough - * nor jansi.strip are set, then ansi sequences will be printed to the output stream. - * This forces the behavior which is by default dependent on the output stream being a real console: if the - * output stream is redirected to a file or through a system pipe, ansi sequences are disabled by default. - * - * @deprecated use {@link #JANSI_MODE} instead - */ - @Deprecated - public static final String JANSI_FORCE = "jansi.force"; - /** - * If the jansi.eager system property is set to true, the system streams will be eagerly - * initialized, else the initialization is delayed until {@link #out()}, {@link #err()} or {@link #systemInstall()} - * is called. - * - * @deprecated this property has been added but only for backward compatibility. - * @since 2.1 - */ - @Deprecated() - public static final String JANSI_EAGER = "jansi.eager"; - /** - * If the jansi.noreset system property is set to true, the attributes won't be - * reset when the streams are uninstalled. - */ public static final String JANSI_NORESET = "jansi.noreset"; - /** - * If the jansi.graceful system property is set to false, any exception that occurs - * during the initialization will cause the library to report this exception and fail. The default - * behavior is to behave gracefully and fall back to pure emulation on posix systems. - */ public static final String JANSI_GRACEFUL = "jansi.graceful"; - /** - * @deprecated this field will be made private in a future release, use {@link #sysOut()} instead - */ - @Deprecated - public static PrintStream system_out = System.out; - /** - * @deprecated this field will be made private in a future release, use {@link #out()} instead - */ - @Deprecated - public static PrintStream out; - /** - * @deprecated this field will be made private in a future release, use {@link #sysErr()} instead - */ - @Deprecated - public static PrintStream system_err = System.err; - /** - * @deprecated this field will be made private in a future release, use {@link #err()} instead - */ - @Deprecated - public static PrintStream err; - - /** - * Try to find the width of the console for this process. - * Both output and error streams will be checked to determine the width. - * A value of 0 is returned if the width can not be determined. - * @since 2.2 - */ - public static int getTerminalWidth() { - int w = out().getTerminalWidth(); - if (w <= 0) { - w = err().getTerminalWidth(); - } - return w; - } - - static final boolean IS_WINDOWS = - System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); + @Deprecated public static final String JANSI_PASSTHROUGH = "jansi.passthrough"; + @Deprecated public static final String JANSI_STRIP = "jansi.strip"; + @Deprecated public static final String JANSI_FORCE = "jansi.force"; - static final boolean IS_CYGWIN = - IS_WINDOWS && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/"); + @Deprecated public static PrintStream system_out = System.out; + @Deprecated public static PrintStream out; + @Deprecated public static PrintStream system_err = System.err; + @Deprecated public static PrintStream err; - static final boolean IS_MSYSTEM = IS_WINDOWS - && System.getenv("MSYSTEM") != null - && (System.getenv("MSYSTEM").startsWith("MINGW") - || System.getenv("MSYSTEM").equals("MSYS")); + private static PrintStream savedSystemOut = System.out; + private static PrintStream savedSystemErr = System.err; - static final boolean IS_CONEMU = IS_WINDOWS && System.getenv("ConEmuPID") != null; - - static final int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; - - static int STDOUT_FILENO = 1; - - static int STDERR_FILENO = 2; - - static { - if (getBoolean(JANSI_EAGER)) { - initStreams(); - } - } - - private static boolean initialized; // synchronized on AnsiConsole.class - private static int installed; // synchronized on AnsiConsole.class - private static int virtualProcessing; // synchronized on AnsiConsole.class + private static final AtomicInteger installCount = new AtomicInteger(0); + private static boolean initialized = false; private AnsiConsole() {} - private static AnsiPrintStream ansiStream(boolean stdout) { - FileDescriptor descriptor = stdout ? FileDescriptor.out : FileDescriptor.err; - final OutputStream out = new FastBufferedOutputStream(new FileOutputStream(descriptor)); - - String enc = System.getProperty(stdout ? "stdout.encoding" : "stderr.encoding"); - if (enc == null) { - enc = System.getProperty(stdout ? "sun.stdout.encoding" : "sun.stderr.encoding"); - } - - final boolean isatty; - boolean isAtty; - boolean withException; - // Do not use the CLibrary.STDOUT_FILENO to avoid errors in case - // the library can not be loaded on unsupported platforms - final int fd = stdout ? STDOUT_FILENO : STDERR_FILENO; - try { - // If we can detect that stdout is not a tty.. then setup - // to strip the ANSI sequences.. - isAtty = isatty(fd) != 0; - String term = System.getenv("TERM"); - String emacs = System.getenv("INSIDE_EMACS"); - if (isAtty && "dumb".equals(term) && emacs != null && !emacs.contains("comint")) { - isAtty = false; - } - withException = false; - } catch (Throwable ignore) { - // These errors happen if the JNI lib is not available for your platform. - // But since we are on ANSI friendly platform, assume the user is on the console. - isAtty = false; - withException = true; - } - isatty = isAtty; - - final AnsiOutputStream.WidthSupplier width; - final AnsiProcessor processor; - final AnsiType type; - final AnsiOutputStream.IoRunnable installer; - final AnsiOutputStream.IoRunnable uninstaller; - if (!isatty) { - processor = null; - type = withException ? AnsiType.Unsupported : AnsiType.Redirected; - installer = uninstaller = null; - width = new AnsiOutputStream.ZeroWidthSupplier(); - } else if (IS_WINDOWS) { - final long console = GetStdHandle(stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE); - final int[] mode = new int[1]; - final boolean isConsole = GetConsoleMode(console, mode) != 0; - final AnsiOutputStream.WidthSupplier kernel32Width = new AnsiOutputStream.WidthSupplier() { - @Override - public int getTerminalWidth() { - CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); - GetConsoleScreenBufferInfo(console, info); - return info.windowWidth(); - } - }; - - if (isConsole && SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) { - SetConsoleMode(console, mode[0]); // set it back for now, but we know it works - processor = null; - type = AnsiType.VirtualTerminal; - installer = () -> { - synchronized (AnsiConsole.class) { - virtualProcessing++; - SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); - } - }; - uninstaller = () -> { - synchronized (AnsiConsole.class) { - if (--virtualProcessing == 0) { - SetConsoleMode(console, mode[0]); - } - } - }; - width = kernel32Width; - } else if ((IS_CONEMU || IS_CYGWIN || IS_MSYSTEM) && !isConsole) { - // ANSI-enabled ConEmu, Cygwin or MSYS(2) on Windows... - processor = null; - type = AnsiType.Native; - installer = uninstaller = null; - MingwSupport mingw = new MingwSupport(); - String name = mingw.getConsoleName(stdout); - if (name != null && !name.isEmpty()) { - width = () -> mingw.getTerminalWidth(name); - } else { - width = () -> -1; - } - } else { - // On Windows, when no ANSI-capable terminal is used, we know the console does not natively interpret - // ANSI - // codes but we can use jansi Kernel32 API for console - AnsiProcessor proc; - AnsiType ttype; - try { - proc = new WindowsAnsiProcessor(out, console); - ttype = AnsiType.Emulation; - } catch (Throwable ignore) { - // this happens when the stdout is being redirected to a file. - // Use the AnsiProcessor to strip out the ANSI escape sequences. - proc = new AnsiProcessor(out); - ttype = AnsiType.Unsupported; - } - processor = proc; - type = ttype; - installer = uninstaller = null; - width = kernel32Width; - } - } - - // We must be on some Unix variant... - else { - // ANSI-enabled ConEmu, Cygwin or MSYS(2) on Windows... - processor = null; - type = AnsiType.Native; - installer = uninstaller = null; - width = new AnsiOutputStream.WidthSupplier() { - @Override - public int getTerminalWidth() { - WinSize sz = new WinSize(); - ioctl(fd, CLibrary.TIOCGWINSZ, sz); - return sz.ws_col; - } - }; - } - - AnsiMode mode; - - // If the jansi.mode property is set, use it - String jansiMode = System.getProperty(stdout ? JANSI_OUT_MODE : JANSI_ERR_MODE, System.getProperty(JANSI_MODE)); - if (JANSI_MODE_FORCE.equals(jansiMode)) { - mode = AnsiMode.Force; - } else if (JANSI_MODE_STRIP.equals(jansiMode)) { - mode = AnsiMode.Strip; - } else if (jansiMode != null) { - mode = isatty ? AnsiMode.Default : AnsiMode.Strip; - } - - // If the jansi.passthrough property is set, then don't interpret - // any of the ansi sequences. - else if (getBoolean(JANSI_PASSTHROUGH)) { - mode = AnsiMode.Force; - } - - // If the jansi.strip property is set, then we just strip the - // the ansi escapes. - else if (getBoolean(JANSI_STRIP)) { - mode = AnsiMode.Strip; - } - - // If the jansi.force property is set, then we force to output - // the ansi escapes for piping it into ansi color aware commands (e.g. less -r) - else if (getBoolean(JANSI_FORCE)) { - mode = AnsiMode.Force; - } else { - mode = isatty ? AnsiMode.Default : AnsiMode.Strip; - } - - AnsiColors colors; - - String colorterm, term; - // If the jansi.colors property is set, use it - String jansiColors = - System.getProperty(stdout ? JANSI_OUT_COLORS : JANSI_ERR_COLORS, System.getProperty(JANSI_COLORS)); - if (JANSI_COLORS_TRUECOLOR.equals(jansiColors)) { - colors = AnsiColors.TrueColor; - } else if (JANSI_COLORS_256.equals(jansiColors)) { - colors = AnsiColors.Colors256; - } else if (jansiColors != null) { - colors = AnsiColors.Colors16; - } - - // If the COLORTERM env variable contains "truecolor" or "24bit", assume true color support - // see https://gist.github.com/XVilka/8346728#true-color-detection - else if ((colorterm = System.getenv("COLORTERM")) != null - && (colorterm.contains("truecolor") || colorterm.contains("24bit"))) { - colors = AnsiColors.TrueColor; - } - - // check the if TERM contains -direct - else if ((term = System.getenv("TERM")) != null && term.contains("-direct")) { - colors = AnsiColors.TrueColor; - } - - // check the if TERM contains -256color - else if (term != null && term.contains("-256color")) { - colors = AnsiColors.Colors256; - } - - // else defaults to 16 colors - else { - colors = AnsiColors.Colors16; - } - - // If the jansi.noreset property is not set, reset the attributes - // when the stream is closed - boolean resetAtUninstall = type != AnsiType.Unsupported && !getBoolean(JANSI_NORESET); - - Charset cs = Charset.defaultCharset(); - if (enc != null) { - try { - cs = Charset.forName(enc); - } catch (UnsupportedCharsetException e) { - } - } - return newPrintStream( - new AnsiOutputStream( - out, width, mode, processor, type, colors, cs, installer, uninstaller, resetAtUninstall), - cs.name()); - } - - private static AnsiPrintStream newPrintStream(AnsiOutputStream out, String enc) { - if (enc != null) { - try { - return new AnsiPrintStream(out, true, enc); - } catch (UnsupportedEncodingException e) { - } - } - return new AnsiPrintStream(out, true); - } - - static boolean getBoolean(String name) { - boolean result = false; - try { - String val = System.getProperty(name); - result = val.isEmpty() || Boolean.parseBoolean(val); - } catch (IllegalArgumentException | NullPointerException ignored) { - } - return result; - } - - /** - * If the standard out natively supports ANSI escape codes, then this just - * returns System.out, otherwise it will provide an ANSI aware PrintStream - * which strips out the ANSI escape sequences or which implement the escape - * sequences. - * - * @return a PrintStream which is ANSI aware. - */ public static AnsiPrintStream out() { initStreams(); return (AnsiPrintStream) out; } - /** - * Access to the original System.out stream before ansi streams were installed. - * - * @return the originial System.out print stream - */ public static PrintStream sysOut() { - return system_out; + return savedSystemOut; } - /** - * If the standard out natively supports ANSI escape codes, then this just - * returns System.err, otherwise it will provide an ANSI aware PrintStream - * which strips out the ANSI escape sequences or which implement the escape - * sequences. - * - * @return a PrintStream which is ANSI aware. - */ public static AnsiPrintStream err() { initStreams(); return (AnsiPrintStream) err; } - /** - * Access to the original System.err stream before ansi streams were installed. - * - * @return the originial System.err print stream - */ public static PrintStream sysErr() { - return system_err; + return savedSystemErr; + } + + public static int getTerminalWidth() { + int w = out().getTerminalWidth(); + if (w <= 0) { + w = err().getTerminalWidth(); + } + return w; } - /** - * Install AnsiConsole.out() to System.out and - * AnsiConsole.err() to System.err. - * @see #systemUninstall() - */ public static synchronized void systemInstall() { - if (installed == 0) { + if (installCount.get() == 0) { initStreams(); try { ((AnsiPrintStream) out).install(); @@ -524,24 +90,15 @@ public static synchronized void systemInstall() { System.setOut(out); System.setErr(err); } - installed++; + installCount.incrementAndGet(); } - /** - * check if the streams have been installed or not - */ public static synchronized boolean isInstalled() { - return installed > 0; + return installCount.get() > 0; } - /** - * undo a previous {@link #systemInstall()}. If {@link #systemInstall()} was called - * multiple times, {@link #systemUninstall()} must be called the same number of times before - * it is actually uninstalled. - */ public static synchronized void systemUninstall() { - installed--; - if (installed == 0) { + if (installCount.decrementAndGet() == 0) { try { ((AnsiPrintStream) out).uninstall(); ((AnsiPrintStream) err).uninstall(); @@ -549,19 +106,16 @@ public static synchronized void systemUninstall() { throw new IOError(e); } initialized = false; - System.setOut(system_out); - System.setErr(system_err); + System.setOut(savedSystemOut); + System.setErr(savedSystemErr); } } - /** - * Initialize the out/err ansi-enabled streams - */ static synchronized void initStreams() { if (!initialized) { - out = ansiStream(true); - err = ansiStream(false); + out = AnsiStreamBuilder.ansiStream(StreamTarget.STDOUT); + err = AnsiStreamBuilder.ansiStream(StreamTarget.STDERR); initialized = true; } } -} +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/AnsiMain.java b/src/main/java/org/fusesource/jansi/AnsiMain.java index 176dcf6c..3f1abcf0 100644 --- a/src/main/java/org/fusesource/jansi/AnsiMain.java +++ b/src/main/java/org/fusesource/jansi/AnsiMain.java @@ -33,7 +33,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.fusesource.jansi.Ansi.ansi; -import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; +// Updated to use the correct structs package containing your refactored classes +import org.fusesource.jansi.internal.struct.*; + /** * Main class for the library, providing executable jar to diagnose Jansi setup. @@ -107,20 +109,23 @@ public static void main(String... args) throws IOException { System.out.println(AnsiConsole.JANSI_COLORS + "= " + System.getProperty(AnsiConsole.JANSI_COLORS, "")); System.out.println(AnsiConsole.JANSI_OUT_COLORS + "= " + System.getProperty(AnsiConsole.JANSI_OUT_COLORS, "")); System.out.println(AnsiConsole.JANSI_ERR_COLORS + "= " + System.getProperty(AnsiConsole.JANSI_ERR_COLORS, "")); + + // Updated to use AnsiPropertyResolver System.out.println( - AnsiConsole.JANSI_PASSTHROUGH + "= " + AnsiConsole.getBoolean(AnsiConsole.JANSI_PASSTHROUGH)); - System.out.println(AnsiConsole.JANSI_STRIP + "= " + AnsiConsole.getBoolean(AnsiConsole.JANSI_STRIP)); - System.out.println(AnsiConsole.JANSI_FORCE + "= " + AnsiConsole.getBoolean(AnsiConsole.JANSI_FORCE)); - System.out.println(AnsiConsole.JANSI_NORESET + "= " + AnsiConsole.getBoolean(AnsiConsole.JANSI_NORESET)); - System.out.println(Ansi.DISABLE + "= " + AnsiConsole.getBoolean(Ansi.DISABLE)); + AnsiConsole.JANSI_PASSTHROUGH + "= " + AnsiPropertyResolver.getBoolean(AnsiConsole.JANSI_PASSTHROUGH)); + System.out.println(AnsiConsole.JANSI_STRIP + "= " + AnsiPropertyResolver.getBoolean(AnsiConsole.JANSI_STRIP)); + System.out.println(AnsiConsole.JANSI_FORCE + "= " + AnsiPropertyResolver.getBoolean(AnsiConsole.JANSI_FORCE)); + System.out.println(AnsiConsole.JANSI_NORESET + "= " + AnsiPropertyResolver.getBoolean(AnsiConsole.JANSI_NORESET)); + System.out.println(Ansi.DISABLE + "= " + AnsiPropertyResolver.getBoolean(Ansi.DISABLE)); System.out.println(); - System.out.println("IS_WINDOWS: " + AnsiConsole.IS_WINDOWS); - if (AnsiConsole.IS_WINDOWS) { - System.out.println("IS_CONEMU: " + AnsiConsole.IS_CONEMU); - System.out.println("IS_CYGWIN: " + AnsiConsole.IS_CYGWIN); - System.out.println("IS_MSYSTEM: " + AnsiConsole.IS_MSYSTEM); + // Updated to use AnsiStreamBuilder + System.out.println("IS_WINDOWS: " + AnsiStreamBuilder.IS_WINDOWS); + if (AnsiStreamBuilder.IS_WINDOWS) { + System.out.println("IS_CONEMU: " + AnsiStreamBuilder.IS_CONEMU); + System.out.println("IS_CYGWIN: " + AnsiStreamBuilder.IS_CYGWIN); + System.out.println("IS_MSYSTEM: " + AnsiStreamBuilder.IS_MSYSTEM); } System.out.println(); @@ -199,11 +204,12 @@ private static String getJansiVersion() { private static void diagnoseTty(boolean stderr) { int isatty; int width; - if (AnsiConsole.IS_WINDOWS) { + // Updated to use AnsiStreamBuilder + if (AnsiStreamBuilder.IS_WINDOWS) { long console = Kernel32.GetStdHandle(stderr ? Kernel32.STD_ERROR_HANDLE : Kernel32.STD_OUTPUT_HANDLE); int[] mode = new int[1]; isatty = Kernel32.GetConsoleMode(console, mode); - if ((AnsiConsole.IS_CONEMU || AnsiConsole.IS_CYGWIN || AnsiConsole.IS_MSYSTEM) && isatty == 0) { + if ((AnsiStreamBuilder.IS_CONEMU || AnsiStreamBuilder.IS_CYGWIN || AnsiStreamBuilder.IS_MSYSTEM) && isatty == 0) { MingwSupport mingw = new MingwSupport(); String name = mingw.getConsoleName(!stderr); if (name != null && !name.isEmpty()) { @@ -214,8 +220,8 @@ private static void diagnoseTty(boolean stderr) { width = 0; } } else { - Kernel32.CONSOLE_SCREEN_BUFFER_INFO info = new Kernel32.CONSOLE_SCREEN_BUFFER_INFO(); - GetConsoleScreenBufferInfo(console, info); + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + Kernel32.GetConsoleScreenBufferInfo(console, info); width = info.windowWidth(); } } else { @@ -341,4 +347,4 @@ private static void closeQuietly(Closeable c) { ioe.printStackTrace(System.err); } } -} +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/AnsiPropertyResolver.java b/src/main/java/org/fusesource/jansi/AnsiPropertyResolver.java new file mode 100644 index 00000000..6cc79d56 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/AnsiPropertyResolver.java @@ -0,0 +1,93 @@ +package org.fusesource.jansi; + +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.logging.Logger; + +final class AnsiPropertyResolver { + + private static final Logger LOG = Logger.getLogger(AnsiPropertyResolver.class.getName()); + + private AnsiPropertyResolver() {} + + static AnsiMode resolveMode(String streamModeKey, boolean isAtty) { + String jansiMode = System.getProperty(streamModeKey, System.getProperty(AnsiConsole.JANSI_MODE)); + + if (jansiMode != null) { + if (AnsiConsole.JANSI_MODE_FORCE.equals(jansiMode)) { + return AnsiMode.Force; + } else if (AnsiConsole.JANSI_MODE_STRIP.equals(jansiMode)) { + return AnsiMode.Strip; + } else { + return isAtty ? AnsiMode.Default : AnsiMode.Strip; + } + } + return resolveLegacyMode(isAtty); + } + + private static AnsiMode resolveLegacyMode(boolean isAtty) { + if (getBoolean(AnsiConsole.JANSI_PASSTHROUGH)) { + return AnsiMode.Force; + } + if (getBoolean(AnsiConsole.JANSI_STRIP)) { + return AnsiMode.Strip; + } + if (getBoolean(AnsiConsole.JANSI_FORCE)) { + return AnsiMode.Force; + } + return isAtty ? AnsiMode.Default : AnsiMode.Strip; + } + + static AnsiColors resolveColors(String streamColorsKey) { + String jansiColors = System.getProperty(streamColorsKey, System.getProperty(AnsiConsole.JANSI_COLORS)); + + if (AnsiConsole.JANSI_COLORS_TRUECOLOR.equals(jansiColors)) { + return AnsiColors.TrueColor; + } else if (AnsiConsole.JANSI_COLORS_256.equals(jansiColors)) { + return AnsiColors.Colors256; + } else if (jansiColors != null) { + return AnsiColors.Colors16; + } + return detectColorsFromEnvironment(); + } + + private static AnsiColors detectColorsFromEnvironment() { + String colorterm = System.getenv("COLORTERM"); + if (colorterm != null && (colorterm.contains("truecolor") || colorterm.contains("24bit"))) { + return AnsiColors.TrueColor; + } + String term = System.getenv("TERM"); + if (term != null && term.contains("-direct")) { + return AnsiColors.TrueColor; + } + if (term != null && term.contains("-256color")) { + return AnsiColors.Colors256; + } + return AnsiColors.Colors16; + } + + static Charset resolveCharset(StreamTarget target) { + String enc = System.getProperty(target.encodingProperty); + if (enc == null) { + enc = System.getProperty(target.legacyEncodingProperty); + } + if (enc != null) { + try { + return Charset.forName(enc); + } catch (UnsupportedCharsetException e) { + LOG.fine("Unsupported charset '" + enc + "' for " + target + ", using default charset: " + e.getMessage()); + } + } + return Charset.defaultCharset(); + } + + static boolean getBoolean(String name) { + try { + String val = System.getProperty(name); + return val != null && (val.isEmpty() || Boolean.parseBoolean(val)); + } catch (IllegalArgumentException e) { + LOG.fine("Invalid boolean property '" + name + "': " + e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/AnsiStreamBuilder.java b/src/main/java/org/fusesource/jansi/AnsiStreamBuilder.java new file mode 100644 index 00000000..4af360b9 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/AnsiStreamBuilder.java @@ -0,0 +1,140 @@ +package org.fusesource.jansi; + +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import org.fusesource.jansi.internal.CLibrary; +import org.fusesource.jansi.internal.CLibrary.WinSize; +import org.fusesource.jansi.internal.struct.CONSOLE_SCREEN_BUFFER_INFO; +import org.fusesource.jansi.internal.MingwSupport; +import org.fusesource.jansi.io.AnsiOutputStream; +import org.fusesource.jansi.io.AnsiProcessor; +import org.fusesource.jansi.io.FastBufferedOutputStream; +import org.fusesource.jansi.io.WindowsAnsiProcessor; + +import static org.fusesource.jansi.internal.CLibrary.ioctl; +import static org.fusesource.jansi.internal.Kernel32.GetConsoleMode; +import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; +import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; +import static org.fusesource.jansi.internal.Kernel32.SetConsoleMode; + +final class AnsiStreamBuilder { + + private static final Logger LOG = Logger.getLogger(AnsiStreamBuilder.class.getName()); + + static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); + static final boolean IS_CYGWIN = IS_WINDOWS && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/"); + static final boolean IS_MSYSTEM = IS_WINDOWS && System.getenv("MSYSTEM") != null + && (System.getenv("MSYSTEM").startsWith("MINGW") || System.getenv("MSYSTEM").equals("MSYS")); + static final boolean IS_CONEMU = IS_WINDOWS && System.getenv("ConEmuPID") != null; + static final int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + + private static final AtomicInteger virtualProcessing = new AtomicInteger(0); + + private AnsiStreamBuilder() {} + + static AnsiPrintStream ansiStream(StreamTarget target) { + final OutputStream rawOut = new FastBufferedOutputStream(new FileOutputStream(target.fileDescriptor)); + + TtyResult tty = TtyDetector.detectTty(target.fd); + + final StreamConfig config; + if (!tty.isAtty) { + AnsiType type = tty.hadException ? AnsiType.Unsupported : AnsiType.Redirected; + config = new StreamConfig(null, type, null, null, new AnsiOutputStream.ZeroWidthSupplier()); + } else if (IS_WINDOWS) { + config = buildWindowsStream(rawOut, target); + } else { + config = buildUnixStream(target.fd); + } + + AnsiMode mode = AnsiPropertyResolver.resolveMode(target.modeProperty, tty.isAtty); + AnsiColors colors = AnsiPropertyResolver.resolveColors(target.colorsProperty); + Charset cs = AnsiPropertyResolver.resolveCharset(target); + + boolean resetAtUninstall = config.type != AnsiType.Unsupported && !AnsiPropertyResolver.getBoolean(AnsiConsole.JANSI_NORESET); + + return newPrintStream( + new AnsiOutputStream( + rawOut, config.width, mode, config.processor, config.type, + colors, cs, config.installer, config.uninstaller, resetAtUninstall), + cs.name()); + } + + private static StreamConfig buildUnixStream(final int fd) { + AnsiOutputStream.WidthSupplier width = () -> { + WinSize sz = new WinSize(); + ioctl(fd, CLibrary.TIOCGWINSZ, sz); + return sz.ws_col; + }; + return new StreamConfig(null, AnsiType.Native, null, null, width); + } + + private static StreamConfig buildWindowsStream(OutputStream out, StreamTarget target) { + final long console = GetStdHandle(target.stdHandle); + final int[] mode = new int[1]; + final boolean isConsole = GetConsoleMode(console, mode) != 0; + + final AnsiOutputStream.WidthSupplier kernel32Width = () -> { + CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); + GetConsoleScreenBufferInfo(console, info); + return info.windowWidth(); + }; + + if (isConsole && SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) { + SetConsoleMode(console, mode[0]); + return new StreamConfig( + null, AnsiType.VirtualTerminal, + () -> { + synchronized (AnsiConsole.class) { + virtualProcessing.getAndIncrement(); + SetConsoleMode(console, mode[0] | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } + }, + () -> { + synchronized (AnsiConsole.class) { + if (virtualProcessing.decrementAndGet() == 0) { + SetConsoleMode(console, mode[0]); + } + } + }, + kernel32Width); + } + + if ((IS_CONEMU || IS_CYGWIN || IS_MSYSTEM) && !isConsole) { + MingwSupport mingw = new MingwSupport(); + String name = mingw.getConsoleName(target.isStdout()); + AnsiOutputStream.WidthSupplier mingwWidth = + (name != null && !name.isEmpty()) ? () -> mingw.getTerminalWidth(name) : () -> -1; + return new StreamConfig(null, AnsiType.Native, null, null, mingwWidth); + } + + AnsiProcessor processor; + AnsiType type; + try { + processor = new WindowsAnsiProcessor(out, console); + type = AnsiType.Emulation; + } catch (Throwable e) { + LOG.fine("WindowsAnsiProcessor unavailable (stream likely redirected): " + e); + processor = new AnsiProcessor(out); + type = AnsiType.Unsupported; + } + return new StreamConfig(processor, type, null, null, kernel32Width); + } + + private static AnsiPrintStream newPrintStream(AnsiOutputStream ansiOut, String enc) { + if (enc != null) { + try { + return new AnsiPrintStream(ansiOut, true, enc); + } catch (UnsupportedEncodingException e) { + LOG.fine("Unsupported encoding '" + enc + "' for AnsiPrintStream: " + e.getMessage()); + } + } + return new AnsiPrintStream(ansiOut, true); + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/StreamConfig.java b/src/main/java/org/fusesource/jansi/StreamConfig.java new file mode 100644 index 00000000..d412688e --- /dev/null +++ b/src/main/java/org/fusesource/jansi/StreamConfig.java @@ -0,0 +1,25 @@ +package org.fusesource.jansi; + +import org.fusesource.jansi.io.AnsiOutputStream; +import org.fusesource.jansi.io.AnsiProcessor; + +final class StreamConfig { + final AnsiProcessor processor; + final AnsiType type; + final AnsiOutputStream.IoRunnable installer; + final AnsiOutputStream.IoRunnable uninstaller; + final AnsiOutputStream.WidthSupplier width; + + StreamConfig( + AnsiProcessor processor, + AnsiType type, + AnsiOutputStream.IoRunnable installer, + AnsiOutputStream.IoRunnable uninstaller, + AnsiOutputStream.WidthSupplier width) { + this.processor = processor; + this.type = type; + this.installer = installer; + this.uninstaller = uninstaller; + this.width = width; + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/StreamTarget.java b/src/main/java/org/fusesource/jansi/StreamTarget.java new file mode 100644 index 00000000..9f08718a --- /dev/null +++ b/src/main/java/org/fusesource/jansi/StreamTarget.java @@ -0,0 +1,53 @@ +package org.fusesource.jansi; + +import java.io.FileDescriptor; +import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; +import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; + +enum StreamTarget { + STDOUT( + 1, + FileDescriptor.out, + "stdout.encoding", + "sun.stdout.encoding", + AnsiConsole.JANSI_OUT_MODE, + AnsiConsole.JANSI_OUT_COLORS, + STD_OUTPUT_HANDLE), + STDERR( + 2, + FileDescriptor.err, + "stderr.encoding", + "sun.stderr.encoding", + AnsiConsole.JANSI_ERR_MODE, + AnsiConsole.JANSI_ERR_COLORS, + STD_ERROR_HANDLE); + + final int fd; + final FileDescriptor fileDescriptor; + final String encodingProperty; + final String legacyEncodingProperty; + final String modeProperty; + final String colorsProperty; + final int stdHandle; + + StreamTarget( + int fd, + FileDescriptor fileDescriptor, + String encodingProperty, + String legacyEncodingProperty, + String modeProperty, + String colorsProperty, + int stdHandle) { + this.fd = fd; + this.fileDescriptor = fileDescriptor; + this.encodingProperty = encodingProperty; + this.legacyEncodingProperty = legacyEncodingProperty; + this.modeProperty = modeProperty; + this.colorsProperty = colorsProperty; + this.stdHandle = stdHandle; + } + + boolean isStdout() { + return this == STDOUT; + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/TtyDetector.java b/src/main/java/org/fusesource/jansi/TtyDetector.java new file mode 100644 index 00000000..bf1b3a89 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/TtyDetector.java @@ -0,0 +1,34 @@ +package org.fusesource.jansi; + +import java.util.logging.Logger; +import static org.fusesource.jansi.internal.CLibrary.isatty; + +final class TtyDetector { + + private static final Logger LOG = Logger.getLogger(TtyDetector.class.getName()); + + private TtyDetector() {} + + static TtyResult detectTty(int fd) { + try { + boolean isAtty = isatty(fd) != 0; + isAtty = adjustForDumbTerminal(isAtty); + return new TtyResult(isAtty, false); + } catch (Throwable e) { + LOG.fine("JNI TTY detection failed for fd=" + fd + ", falling back to non-TTY: " + e); + return new TtyResult(false, true); + } + } + + private static boolean adjustForDumbTerminal(boolean isAtty) { + if (!isAtty) { + return false; + } + String term = System.getenv("TERM"); + String emacs = System.getenv("INSIDE_EMACS"); + if ("dumb".equals(term) && emacs != null && !emacs.contains("comint")) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/TtyResult.java b/src/main/java/org/fusesource/jansi/TtyResult.java new file mode 100644 index 00000000..e2b5690c --- /dev/null +++ b/src/main/java/org/fusesource/jansi/TtyResult.java @@ -0,0 +1,11 @@ +package org.fusesource.jansi; + +final class TtyResult { + final boolean isAtty; + final boolean hadException; + + TtyResult(boolean isAtty, boolean hadException) { + this.isAtty = isAtty; + this.hadException = hadException; + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/WindowsSupport.java b/src/main/java/org/fusesource/jansi/WindowsSupport.java index ac031395..866af3f7 100644 --- a/src/main/java/org/fusesource/jansi/WindowsSupport.java +++ b/src/main/java/org/fusesource/jansi/WindowsSupport.java @@ -25,11 +25,33 @@ public class WindowsSupport { @Deprecated public static String getLastErrorMessage() { - return Kernel32.getLastErrorMessage(); + // Get the raw integer error code first + int errorCode = Kernel32.GetLastError(); + return getErrorMessage(errorCode); } @Deprecated public static String getErrorMessage(int errorCode) { - return Kernel32.getErrorMessage(errorCode); + int bufferSize = 1024; + byte[] buffer = new byte[bufferSize]; // Buffer for UTF-16LE characters + + // Ask Windows to format the integer error code into a human-readable string + int charsWritten = Kernel32.FormatMessageW( + Kernel32.FORMAT_MESSAGE_FROM_SYSTEM, + 0, + errorCode, + 0, + buffer, + bufferSize, + null + ); + + if (charsWritten == 0) { + return "Unknown error code: " + errorCode; + } + + // Convert the native byte array back into a standard Java String + // Multiplied by 2 because UTF-16LE uses 2 bytes per character + return new String(buffer, 0, charsWritten * 2, java.nio.charset.StandardCharsets.UTF_16LE).trim(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/CLibrary.java b/src/main/java/org/fusesource/jansi/internal/CLibrary.java index 2e2285c3..459cb3f7 100644 --- a/src/main/java/org/fusesource/jansi/internal/CLibrary.java +++ b/src/main/java/org/fusesource/jansi/internal/CLibrary.java @@ -16,103 +16,178 @@ package org.fusesource.jansi.internal; /** - * Interface to access some low level POSIX functions, loaded by + * JNI bridge to low level POSIX functions, loaded by * HawtJNI Runtime - * as jansi library. + * as the {@code jansi} native library. + * + *

Always check {@link #LOADED} before calling a native method directly. + * For the common terminal-detection idioms, prefer {@link PosixTerminal}'s + * typed helpers instead of calling {@link #isatty} / {@link #ioctl} directly.

+ * + *

Note: {@link WinSize} and {@link Termios} must remain nested + * inside this class — the native library binds to them by their JNI nested + * name ({@code CLibrary$WinSize}, {@code CLibrary$Termios}). Moving them to + * top-level classes would break the native binding.

* * @see JansiLoader + * @see PosixTerminal + * @see Kernel32 */ @SuppressWarnings("unused") public class CLibrary { - // + // ───────────────────────────────────────────────────────── // Initialization - // + // ───────────────────────────────────────────────────────── + /** + * {@code true} if the native jansi library loaded successfully and the + * native methods on this class are safe to call. + */ public static final boolean LOADED; static { - LOADED = JansiLoader.initialize(); - if (LOADED) { - init(); - } + LOADED = NativeLoader.loadAndInit(CLibrary::init, CLibrary.class.getName()); } private static native void init(); - // - // Constants - // + private CLibrary() {} - public static int STDOUT_FILENO = 1; + // ───────────────────────────────────────────────────────── + // Standard file descriptors + // (fixed: hardcoded literals are now `final` — they are not + // populated by native init(), so there's no reason they were mutable) + // ───────────────────────────────────────────────────────── - public static int STDERR_FILENO = 2; + /** POSIX standard output file descriptor ({@code STDOUT_FILENO}). */ + public static final int STDOUT_FILENO = 1; + /** POSIX standard error file descriptor ({@code STDERR_FILENO}). */ + public static final int STDERR_FILENO = 2; + + // ───────────────────────────────────────────────────────── + // Feature flags — populated by native init() for the current platform + // ───────────────────────────────────────────────────────── + + /** {@code true} if the platform provides {@code isatty()}. */ public static boolean HAVE_ISATTY; + /** {@code true} if the platform provides {@code ttyname()}. */ public static boolean HAVE_TTYNAME; + // ───────────────────────────────────────────────────────── + // tcsetattr() "optional_actions" values + // ───────────────────────────────────────────────────────── + + /** Apply terminal attribute changes immediately. */ public static int TCSANOW; + + /** Apply changes only after all pending output has been transmitted. */ public static int TCSADRAIN; + + /** Apply changes after pending output is transmitted, and discard pending input. */ public static int TCSAFLUSH; + + // ───────────────────────────────────────────────────────── + // ioctl / termios request codes — platform-specific, populated by native init() + // ───────────────────────────────────────────────────────── + + /** Request code to get terminal attributes (alternate of {@code tcgetattr}). */ public static long TIOCGETA; + + /** Request code to set terminal attributes (alternate of {@code tcsetattr}). */ public static long TIOCSETA; + + /** Request code to get the current line discipline. */ public static long TIOCGETD; + + /** Request code to set the current line discipline. */ public static long TIOCSETD; + + /** Request code to get the terminal window size ({@code TIOCGWINSZ}). */ public static long TIOCGWINSZ; + + /** Request code to set the terminal window size ({@code TIOCSWINSZ}). */ public static long TIOCSWINSZ; + // ───────────────────────────────────────────────────────── + // Native methods + // ───────────────────────────────────────────────────────── + /** - * test whether a file descriptor refers to a terminal + * Tests whether a file descriptor refers to a terminal. * * @param fd file descriptor - * @return isatty() returns 1 if fd is an open file descriptor referring to a - * terminal; otherwise 0 is returned, and errno is set to indicate the - * error - * @see ISATTY(3) man-page - * @see ISATTY(3P) man-page + * @return {@code 1} if {@code fd} is open and refers to a terminal; otherwise + * {@code 0}, with {@code errno} set to indicate the error + * @see isatty(3) */ public static native int isatty(int fd); + /** + * Returns the pathname of the terminal associated with a file descriptor, + * or {@code null} if {@code filedes} is not connected to one. + * + * @see ttyname(3) + */ public static native String ttyname(int filedes); /** - * The openpty() function finds an available pseudoterminal and returns - * file descriptors for the master and slave in amaster and aslave. + * Finds an available pseudoterminal and returns file descriptors for the + * master and slave sides. * * @param amaster master return value * @param aslave slave return value * @param name filename return value * @param termios optional pty attributes * @param winsize optional size - * @return 0 on success - * @see OPENPTY(3) man-page + * @return {@code 0} on success + * @see openpty(3) */ public static native int openpty(int[] amaster, int[] aslave, byte[] name, Termios termios, WinSize winsize); + /** + * Gets the terminal attributes for {@code filedes} into {@code termios}. + * + * @return {@code 0} on success; {@code -1} on error + */ public static native int tcgetattr(int filedes, Termios termios); + /** + * Sets the terminal attributes for {@code filedes}. + * + * @param optional_actions one of {@link #TCSANOW}, {@link #TCSADRAIN}, {@link #TCSAFLUSH} + * @return {@code 0} on success; {@code -1} on error + */ public static native int tcsetattr(int filedes, int optional_actions, Termios termios); /** - * Control a STREAMS device. + * Generic device control call. * - * @see IOCTL(3P) man-page + * @see ioctl(3p) */ public static native int ioctl(int filedes, long request, int[] params); + /** + * Device control call specialized for {@link WinSize}, typically used with + * {@link #TIOCGWINSZ} / {@link #TIOCSWINSZ}. + */ public static native int ioctl(int filedes, long request, WinSize params); + // ───────────────────────────────────────────────────────── + // Nested structs — must stay nested; see class-level note above + // ───────────────────────────────────────────────────────── + /** - * Window sizes. + * Maps to the POSIX {@code struct winsize}. * - * @see IOCTL_TTY(2) man-page + * @see ioctl_tty(4) */ public static class WinSize { static { - JansiLoader.initialize(); - init(); + NativeLoader.loadAndInit(WinSize::init, WinSize.class.getName()); } private static native void init(); @@ -130,19 +205,23 @@ public WinSize(short ws_row, short ws_col) { this.ws_row = ws_row; this.ws_col = ws_col; } + + @Override + public String toString() { + return "WinSize{rows=" + ws_row + ", cols=" + ws_col + "}"; + } } /** - * termios structure for termios functions, describing a general terminal interface that is - * provided to control asynchronous communications ports + * Maps to the POSIX {@code struct termios}, describing the general + * terminal interface used to control asynchronous communications ports. * - * @see TERMIOS(3) man-page + * @see termios(3) */ public static class Termios { static { - JansiLoader.initialize(); - init(); + NativeLoader.loadAndInit(Termios::init, Termios.class.getName()); } private static native void init(); @@ -156,5 +235,11 @@ public static class Termios { public byte[] c_cc = new byte[32]; public long c_ispeed; public long c_ospeed; + + @Override + public String toString() { + return "Termios{iflag=" + c_iflag + ", oflag=" + c_oflag + ", cflag=" + c_cflag + ", lflag=" + c_lflag + + "}"; + } } -} +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/JansiLoader.java b/src/main/java/org/fusesource/jansi/internal/JansiLoader.java index ef9d8e7b..212c85c2 100644 --- a/src/main/java/org/fusesource/jansi/internal/JansiLoader.java +++ b/src/main/java/org/fusesource/jansi/internal/JansiLoader.java @@ -1,90 +1,44 @@ -/* - * Copyright (C) 2009-2023 the original author(s). - * - * Licensed 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. - */ package org.fusesource.jansi.internal; -/*-------------------------------------------------------------------------- - * Copyright 2007 Taro L. Saito - * - * Licensed 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. - *--------------------------------------------------------------------------*/ - import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; -import java.util.Properties; -import java.util.Random; +import java.util.UUID; +import java.util.logging.Logger; import org.fusesource.jansi.AnsiConsole; +import org.fusesource.jansi.internal.loader.config.JansiEnvironment; +import org.fusesource.jansi.internal.loader.io.ContentVerifier; +import org.fusesource.jansi.internal.loader.io.LibraryExtractor; +import org.fusesource.jansi.internal.loader.io.TempFileCleanup; +import org.fusesource.jansi.internal.loader.resolution.LibraryResolver; -/** - * Set the system properties, org.jansi.lib.path, org.jansi.lib.name, - * appropriately so that jansi can find *.dll, *.jnilib and - * *.so files, according to the current OS (win, linux, mac). - *

- * The library files are automatically extracted from this project's package - * (JAR). - *

- * usage: call {@link #initialize()} before using Jansi. - */ public class JansiLoader { + private static final Logger LOG = Logger.getLogger(JansiLoader.class.getName()); + private static final String LIBRARY_PATH_PROPERTY = "library.jansi.path"; + private static boolean loaded = false; private static String nativeLibraryPath; private static String nativeLibrarySourceUrl; - /** - * Loads Jansi native library. - * - * @return True if jansi native library is successfully loaded; false - * otherwise. - */ + private JansiLoader() {} + public static synchronized boolean initialize() { - // only cleanup before the first extract if (!loaded) { - Thread cleanup = new Thread(JansiLoader::cleanup, "cleanup"); - cleanup.setPriority(Thread.MIN_PRIORITY); - cleanup.setDaemon(true); - cleanup.start(); + TempFileCleanup.cleanup(JansiEnvironment.getTempDir(), JansiEnvironment.getVersion()); } try { loadJansiNativeLibrary(); } catch (Exception e) { - if (!Boolean.parseBoolean(System.getProperty(AnsiConsole.JANSI_GRACEFUL, "true"))) { - throw new RuntimeException( - "Unable to load jansi native library. You may want set the `jansi.graceful` system property to true to be able to use Jansi on your platform", - e); + boolean graceful = Boolean.parseBoolean( + System.getProperty(AnsiConsole.JANSI_GRACEFUL, "true")); + if (!graceful) { + throw new RuntimeException("Unable to load jansi native library.", e); } + LOG.warning("Failed to load jansi native library: " + e.getMessage()); } return loaded; } @@ -97,255 +51,37 @@ public static String getNativeLibrarySourceUrl() { return nativeLibrarySourceUrl; } - private static File getTempDir() { - return new File(System.getProperty("jansi.tmpdir", System.getProperty("java.io.tmpdir"))); - } - - /** - * Deleted old native libraries e.g. on Windows the DLL file is not removed - * on VM-Exit (bug #80) - */ - static void cleanup() { - String tempFolder = getTempDir().getAbsolutePath(); - File dir = new File(tempFolder); - - File[] nativeLibFiles = dir.listFiles(new FilenameFilter() { - private final String searchPattern = "jansi-" + getVersion(); - - public boolean accept(File dir, String name) { - return name.startsWith(searchPattern) && !name.endsWith(".lck"); - } - }); - if (nativeLibFiles != null) { - for (File nativeLibFile : nativeLibFiles) { - File lckFile = new File(nativeLibFile.getAbsolutePath() + ".lck"); - if (!lckFile.exists()) { - try { - nativeLibFile.delete(); - } catch (SecurityException e) { - System.err.println("Failed to delete old native lib" + e.getMessage()); - } - } - } - } - } - - private static int readNBytes(InputStream in, byte[] b) throws IOException { - int n = 0; - int len = b.length; - while (n < len) { - int count = in.read(b, n, len - n); - if (count <= 0) break; - n += count; - } - return n; - } - - private static String contentsEquals(InputStream in1, InputStream in2) throws IOException { - byte[] buffer1 = new byte[8192]; - byte[] buffer2 = new byte[8192]; - int numRead1; - int numRead2; - while (true) { - numRead1 = readNBytes(in1, buffer1); - numRead2 = readNBytes(in2, buffer2); - if (numRead1 > 0) { - if (numRead2 <= 0) { - return "EOF on second stream but not first"; - } - if (numRead2 != numRead1) { - return "Read size different (" + numRead1 + " vs " + numRead2 + ")"; - } - // Otherwise same number of bytes read - for (int i = 0; i < numRead1; i++) { - if (buffer1[i] != buffer2[i]) { - return "Content differs"; - } - } - // Otherwise same bytes read, so continue ... - } else { - // Nothing more in stream 1 ... - if (numRead2 > 0) { - return "EOF on first stream but not second"; - } else { - return null; - } - } - } - } - - /** - * Extracts and loads the specified library file to the target folder - * - * @param libFolderForCurrentOS Library path. - * @param libraryFileName Library name. - * @param targetFolder Target folder. - * @return - */ - private static boolean extractAndLoadLibraryFile( - String libFolderForCurrentOS, String libraryFileName, String targetFolder) { - String nativeLibraryFilePath = libFolderForCurrentOS + "/" + libraryFileName; - // Include architecture name in temporary filename in order to avoid conflicts - // when multiple JVMs with different architectures running at the same time - String uuid = randomUUID(); - String extractedLibFileName = String.format("jansi-%s-%s-%s", getVersion(), uuid, libraryFileName); - String extractedLckFileName = extractedLibFileName + ".lck"; - - File extractedLibFile = new File(targetFolder, extractedLibFileName); - File extractedLckFile = new File(targetFolder, extractedLckFileName); - - try { - // Extract a native library file into the target directory - try (InputStream in = JansiLoader.class.getResourceAsStream(nativeLibraryFilePath)) { - if (!extractedLckFile.exists()) { - new FileOutputStream(extractedLckFile).close(); - } - Files.copy(in, extractedLibFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } finally { - // Delete the extracted lib file on JVM exit. - extractedLibFile.deleteOnExit(); - extractedLckFile.deleteOnExit(); - } - - // Set executable (x) flag to enable Java to load the native library - extractedLibFile.setReadable(true); - extractedLibFile.setWritable(true); - extractedLibFile.setExecutable(true); - - // Check whether the contents are properly copied from the resource folder - try (InputStream nativeIn = JansiLoader.class.getResourceAsStream(nativeLibraryFilePath)) { - try (InputStream extractedLibIn = new FileInputStream(extractedLibFile)) { - String eq = contentsEquals(nativeIn, extractedLibIn); - if (eq != null) { - throw new RuntimeException(String.format( - "Failed to write a native library file at %s because %s", extractedLibFile, eq)); - } - } - } - - // Load library - if (loadNativeLibrary(extractedLibFile)) { - nativeLibrarySourceUrl = - JansiLoader.class.getResource(nativeLibraryFilePath).toExternalForm(); - return true; - } - } catch (IOException e) { - System.err.println(e.getMessage()); - } - return false; - } - - private static String randomUUID() { - return Long.toHexString(new Random().nextLong()); - } - - /** - * Loads native library using the given path and name of the library. - * - * @param libPath Path of the native library. - * @return True for successfully loading; false otherwise. - */ - private static boolean loadNativeLibrary(File libPath) { - if (libPath.exists()) { - try { - String path = libPath.getAbsolutePath(); - System.load(path); - nativeLibraryPath = path; - return true; - } catch (UnsatisfiedLinkError e) { - if (!libPath.canExecute()) { - // NOTE: this can be tested using something like: - // docker run --rm --tmpfs /tmp -v $PWD:/jansi openjdk:11 java -jar - // /jansi/target/jansi-xxx-SNAPSHOT.jar - System.err.printf( - "Failed to load native library:%s. The native library file at %s is not executable, " - + "make sure that the directory is mounted on a partition without the noexec flag, or set the " - + "jansi.tmpdir system property to point to a proper location. osinfo: %s%n", - libPath.getName(), libPath, OSInfo.getNativeLibFolderPathForCurrentOS()); - } else { - System.err.printf( - "Failed to load native library:%s. osinfo: %s%n", - libPath.getName(), OSInfo.getNativeLibFolderPathForCurrentOS()); - } - System.err.println(e); - return false; - } - - } else { - return false; - } - } - - /** - * Loads jansi library using given path and name of the library. - * - * @throws - */ private static void loadJansiNativeLibrary() throws Exception { - if (loaded) { - return; - } - - List triedPaths = new LinkedList(); + if (loaded) return; - // Try loading library from library.jansi.path library path */ - String jansiNativeLibraryPath = System.getProperty("library.jansi.path"); - String jansiNativeLibraryName = System.getProperty("library.jansi.name"); - if (jansiNativeLibraryName == null) { - jansiNativeLibraryName = System.mapLibraryName("jansi"); - assert jansiNativeLibraryName != null; - if (jansiNativeLibraryName.endsWith(".dylib")) { - jansiNativeLibraryName = jansiNativeLibraryName.replace(".dylib", ".jnilib"); - } - } + List triedPaths = new ArrayList<>(); + String libraryName = LibraryResolver.resolveLibraryName(); - if (jansiNativeLibraryPath != null) { - String withOs = jansiNativeLibraryPath + "/" + OSInfo.getNativeLibFolderPathForCurrentOS(); - if (loadNativeLibrary(new File(withOs, jansiNativeLibraryName))) { - loaded = true; - return; - } else { - triedPaths.add(withOs); - } + // Path 1: User-defined system property + String userDefinedPath = System.getProperty(LIBRARY_PATH_PROPERTY); + if (userDefinedPath != null) { + String withOsSubfolder = userDefinedPath + "/" + OSInfo.getNativeLibFolderPathForCurrentOS(); + if (performSystemLoad(new File(withOsSubfolder, libraryName))) return; + triedPaths.add(withOsSubfolder); - if (loadNativeLibrary(new File(jansiNativeLibraryPath, jansiNativeLibraryName))) { - loaded = true; - return; - } else { - triedPaths.add(jansiNativeLibraryPath); - } + if (performSystemLoad(new File(userDefinedPath, libraryName))) return; + triedPaths.add(userDefinedPath); } - // Load the os-dependent library from the jar file - String packagePath = JansiLoader.class.getPackage().getName().replace('.', '/'); - jansiNativeLibraryPath = - String.format("/%s/native/%s", packagePath, OSInfo.getNativeLibFolderPathForCurrentOS()); - boolean hasNativeLib = hasResource(jansiNativeLibraryPath + "/" + jansiNativeLibraryName); - - if (hasNativeLib) { - // temporary library folder - String tempFolder = getTempDir().getAbsolutePath(); - // Try extracting the library from jar - if (extractAndLoadLibraryFile(jansiNativeLibraryPath, jansiNativeLibraryName, tempFolder)) { - loaded = true; - return; - } else { - triedPaths.add(jansiNativeLibraryPath); - } + // Path 2: Bundled inside the JAR + String jarLibraryPath = LibraryResolver.resolvePackagePath(); + if (JansiLoader.class.getResource(jarLibraryPath + "/" + libraryName) != null) { + String tempFolder = JansiEnvironment.getTempDir().getAbsolutePath(); + if (extractAndLoad(jarLibraryPath, libraryName, tempFolder)) return; + triedPaths.add(jarLibraryPath); } - // As a last resort try from java.library.path + // Path 3: Standard java.library.path String javaLibraryPath = System.getProperty("java.library.path", ""); for (String ldPath : javaLibraryPath.split(File.pathSeparator)) { - if (ldPath.isEmpty()) { - continue; - } - if (loadNativeLibrary(new File(ldPath, jansiNativeLibraryName))) { - loaded = true; - return; - } else { - triedPaths.add(ldPath); - } + if (ldPath.isEmpty()) continue; + if (performSystemLoad(new File(ldPath, libraryName))) return; + triedPaths.add(ldPath); } throw new Exception(String.format( @@ -353,44 +89,41 @@ private static void loadJansiNativeLibrary() throws Exception { OSInfo.getOSName(), OSInfo.getArchName(), String.join(File.pathSeparator, triedPaths))); } - private static boolean hasResource(String path) { - return JansiLoader.class.getResource(path) != null; - } - - /** - * @return The major version of the jansi library. - */ - public static int getMajorVersion() { - String[] c = getVersion().split("\\."); - return (c.length > 0) ? Integer.parseInt(c[0]) : 1; - } + private static boolean extractAndLoad(String libFolder, String libraryFileName, String targetFolder) { + String resourcePath = libFolder + "/" + libraryFileName; + String uniqueName = String.format("jansi-%s-%s-%s", + JansiEnvironment.getVersion(), UUID.randomUUID(), libraryFileName); - /** - * @return The minor version of the jansi library. - */ - public static int getMinorVersion() { - String[] c = getVersion().split("\\."); - return (c.length > 1) ? Integer.parseInt(c[1]) : 0; - } + File extractedLibFile = new File(targetFolder, uniqueName); + File extractedLckFile = new File(targetFolder, uniqueName + ".lck"); - /** - * @return The version of the jansi library. - */ - public static String getVersion() { + try (InputStream extractStream = JansiLoader.class.getResourceAsStream(resourcePath); + InputStream verifyStream = JansiLoader.class.getResourceAsStream(resourcePath)) { - URL versionFile = JansiLoader.class.getResource("/org/fusesource/jansi/jansi.properties"); + LibraryExtractor.extractLibraryToFile(extractStream, extractedLibFile, extractedLckFile); + LibraryExtractor.setExecutablePermissions(extractedLibFile); + ContentVerifier.verifyExtractedContents(verifyStream, extractedLibFile); - String version = "unknown"; - try { - if (versionFile != null) { - Properties versionData = new Properties(); - versionData.load(versionFile.openStream()); - version = versionData.getProperty("version", version); - version = version.trim().replaceAll("[^0-9.]", ""); + if (performSystemLoad(extractedLibFile)) { + nativeLibrarySourceUrl = JansiLoader.class.getResource(resourcePath).toExternalForm(); + return true; } } catch (IOException e) { - System.err.println(e); + LOG.warning("Failed to extract or verify native library: " + e.getMessage()); + } + return false; + } + + private static boolean performSystemLoad(File libFile) { + if (!libFile.exists()) return false; + try { + System.load(libFile.getAbsolutePath()); + nativeLibraryPath = libFile.getAbsolutePath(); + loaded = true; + return true; + } catch (UnsatisfiedLinkError e) { + LOG.warning("Failed to load native library '" + libFile.getName() + "': " + e.getMessage()); + return false; } - return version; } -} +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/Kernel32.java b/src/main/java/org/fusesource/jansi/internal/Kernel32.java index d4884caf..6494b314 100644 --- a/src/main/java/org/fusesource/jansi/internal/Kernel32.java +++ b/src/main/java/org/fusesource/jansi/internal/Kernel32.java @@ -15,33 +15,65 @@ */ package org.fusesource.jansi.internal; -import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.util.logging.Logger; +import org.fusesource.jansi.internal.struct.*; /** - * Interface to access Win32 base APIs. + * Windows-only JNI bridge to the Win32 Kernel32 console API. * - * @see JansiLoader + *

This class exposes a curated subset of the Win32 console functions needed + * by jansi to control terminal colors, cursor position, screen clearing, and + * Virtual Terminal Processing on Windows 10/11. All methods are native and + * loaded from the bundled {@code jansi} JNI library via {@link JansiLoader}.

+ * + *

On non-Windows platforms this class is present in the JAR but the native + * library will not load; callers should check {@link #LOADED} before invoking + * any native method.

+ * + * @author Hiram Chirino + * @since 1.0 + * @see CLibrary + * @see + * Win32 Console Functions (MSDN) */ -@SuppressWarnings("unused") public class Kernel32 { + private static final Logger LOG = Logger.getLogger(Kernel32.class.getName()); + + public static final boolean LOADED; + static { - if (JansiLoader.initialize()) { + boolean loaded = false; + try { + JansiLoader.initialize(); init(); + loaded = true; + } catch (Throwable e) { + LOG.fine("Kernel32 native library could not be loaded " + + "(expected on non-Windows platforms): " + e); } + LOADED = loaded; } private static native void init(); + private Kernel32() {} + + public static int STD_INPUT_HANDLE; + public static int STD_OUTPUT_HANDLE; + public static int STD_ERROR_HANDLE; + public static long INVALID_HANDLE_VALUE; + public static short FOREGROUND_BLUE; public static short FOREGROUND_GREEN; public static short FOREGROUND_RED; public static short FOREGROUND_INTENSITY; + public static short BACKGROUND_BLUE; public static short BACKGROUND_GREEN; public static short BACKGROUND_RED; public static short BACKGROUND_INTENSITY; + public static short COMMON_LVB_LEADING_BYTE; public static short COMMON_LVB_TRAILING_BYTE; public static short COMMON_LVB_GRID_HORIZONTAL; @@ -49,488 +81,91 @@ public class Kernel32 { public static short COMMON_LVB_GRID_RVERTICAL; public static short COMMON_LVB_REVERSE_VIDEO; public static short COMMON_LVB_UNDERSCORE; - public static int FORMAT_MESSAGE_FROM_SYSTEM; - public static int STD_INPUT_HANDLE; - public static int STD_OUTPUT_HANDLE; - public static int STD_ERROR_HANDLE; - public static int INVALID_HANDLE_VALUE; - - public static native long malloc(long size); - - public static native void free(long ptr); - - /** - * http://msdn.microsoft.com/en-us/library/ms686311%28VS.85%29.aspx - */ - public static class SMALL_RECT { - static { - JansiLoader.initialize(); - init(); - } - - private static native void init(); - - public static int SIZEOF; - - public short left; - public short top; - public short right; - public short bottom; - - public short width() { - return (short) (right - left); - } - - public short height() { - return (short) (bottom - top); - } - - public SMALL_RECT copy() { - SMALL_RECT rc = new SMALL_RECT(); - rc.left = left; - rc.top = top; - rc.right = right; - rc.bottom = bottom; - return rc; - } - } - /** - * see http://msdn.microsoft.com/en-us/library/ms686047%28VS.85%29.aspx - */ - public static native int SetConsoleTextAttribute(long consoleOutput, short attributes); + public static int ENABLE_VIRTUAL_TERMINAL_PROCESSING; + public static int ENABLE_PROCESSED_OUTPUT; + public static int ENABLE_WRAP_AT_EOL_OUTPUT; - public static class COORD { - - static { - JansiLoader.initialize(); - init(); - } - - private static native void init(); - - public static int SIZEOF; - - public short x; - public short y; - - public COORD copy() { - COORD rc = new COORD(); - rc.x = x; - rc.y = y; - return rc; - } - } - - /** - * http://msdn.microsoft.com/en-us/library/ms682093%28VS.85%29.aspx - */ - public static class CONSOLE_SCREEN_BUFFER_INFO { - - static { - JansiLoader.initialize(); - init(); - } - - private static native void init(); - - public static int SIZEOF; - - public COORD size = new COORD(); - public COORD cursorPosition = new COORD(); - public short attributes; - public SMALL_RECT window = new SMALL_RECT(); - public COORD maximumWindowSize = new COORD(); - - public int windowWidth() { - return window.width() + 1; - } - - public int windowHeight() { - return window.height() + 1; - } - } + public static int FORMAT_MESSAGE_FROM_SYSTEM; - // DWORD WINAPI WaitForSingleObject( - // _In_ HANDLE hHandle, - // _In_ DWORD dwMilliseconds - // ); - public static native int WaitForSingleObject(long hHandle, int dwMilliseconds); + public static short KEY_EVENT; + public static short MOUSE_EVENT; + public static short WINDOW_BUFFER_SIZE_EVENT; + public static short FOCUS_EVENT; + public static short MENU_EVENT; - /** - * see: http://msdn.microsoft.com/en-us/library/ms724211%28VS.85%29.aspx - */ - public static native int CloseHandle(long handle); + public static native long GetStdHandle(int nStdHandle); - /** - * see: http://msdn.microsoft.com/en-us/library/ms679360(VS.85).aspx - */ - public static native int GetLastError(); + public static native int GetConsoleMode(long hConsoleHandle, int[] lpMode); - public static native int FormatMessageW( - int flags, long source, int messageId, int languageId, byte[] buffer, int size, long[] args); + public static native int SetConsoleMode(long hConsoleHandle, int dwMode); - /** - * See: http://msdn.microsoft.com/en-us/library/ms683171%28VS.85%29.aspx - */ public static native int GetConsoleScreenBufferInfo( - long consoleOutput, CONSOLE_SCREEN_BUFFER_INFO consoleScreenBufferInfo); + long hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo); - /** - * see: http://msdn.microsoft.com/en-us/library/ms683231%28VS.85%29.aspx - */ - public static native long GetStdHandle(int stdHandle); + public static native int SetConsoleTextAttribute(long hConsoleOutput, short wAttributes); - /** - * http://msdn.microsoft.com/en-us/library/ms686025%28VS.85%29.aspx - */ - public static native int SetConsoleCursorPosition(long consoleOutput, COORD cursorPosition); + public static native int SetConsoleTitleW(byte[] lpConsoleTitle); + + public static native int SetConsoleCursorPosition(long hConsoleOutput, COORD dwCursorPosition); - /** - * see: http://msdn.microsoft.com/en-us/library/ms682663%28VS.85%29.aspx - */ public static native int FillConsoleOutputCharacterW( - long consoleOutput, char character, int length, COORD writeCoord, int[] numberOfCharsWritten); + long hConsoleOutput, char cCharacter, int nLength, + COORD dwWriteCoord, int[] lpNumberOfCharsWritten); - /** - * see: https://msdn.microsoft.com/en-us/library/ms682662%28VS.85%29.aspx - */ public static native int FillConsoleOutputAttribute( - long consoleOutput, short attribute, int length, COORD writeCoord, int[] numberOfAttrsWritten); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms687401(v=VS.85).aspx - */ - public static native int WriteConsoleW( - long consoleOutput, char[] buffer, int numberOfCharsToWrite, int[] numberOfCharsWritten, long reserved); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms683167%28VS.85%29.aspx - */ - public static native int GetConsoleMode(long handle, int[] mode); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms686033%28VS.85%29.aspx - */ - public static native int SetConsoleMode(long handle, int mode); - - /** - * see: http://msdn.microsoft.com/en-us/library/078sfkak(VS.80).aspx - */ - public static native int _getch(); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms686050%28VS.85%29.aspx - * - * @return 0 if title was set successfully - */ - public static native int SetConsoleTitle(String title); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms683169(v=VS.85).aspx - * - * @return the current output code page - */ - public static native int GetConsoleOutputCP(); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms686036(v=VS.85).aspx - * - * @return non 0 if code page was set - */ - public static native int SetConsoleOutputCP(int codePageID); - - /** - * see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682013(v=vs.85).aspx - */ - public static class CHAR_INFO { + long hConsoleOutput, short wAttribute, int nLength, + COORD dwWriteCoord, int[] lpNumberOfAttrsWritten); - static { - JansiLoader.initialize(); - init(); - } - - private static native void init(); - - public static int SIZEOF; - - public short attributes; - public char unicodeChar; - } - - /** - * see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms685107(v=vs.85).aspx - */ public static native int ScrollConsoleScreenBuffer( - long consoleOutput, - SMALL_RECT scrollRectangle, - SMALL_RECT clipRectangle, - COORD destinationOrigin, - CHAR_INFO fill); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms684166(v=VS.85).aspx - */ - public static class KEY_EVENT_RECORD { - - static { - JansiLoader.initialize(); - init(); - } - - private static native void init(); - - public static int SIZEOF; - public static int CAPSLOCK_ON; - public static int NUMLOCK_ON; - public static int SCROLLLOCK_ON; - public static int ENHANCED_KEY; - public static int LEFT_ALT_PRESSED; - public static int LEFT_CTRL_PRESSED; - public static int RIGHT_ALT_PRESSED; - public static int RIGHT_CTRL_PRESSED; - public static int SHIFT_PRESSED; - - public boolean keyDown; - public short repeatCount; - public short keyCode; - public short scanCode; - public char uchar; - public int controlKeyState; - - public String toString() { - return "KEY_EVENT_RECORD{" + "keyDown=" - + keyDown + ", repeatCount=" - + repeatCount + ", keyCode=" - + keyCode + ", scanCode=" - + scanCode + ", uchar=" - + uchar + ", controlKeyState=" - + controlKeyState + '}'; - } - } - - /** - * see: http://msdn.microsoft.com/en-us/library/ms684239(v=VS.85).aspx - */ - public static class MOUSE_EVENT_RECORD { - - static { - JansiLoader.initialize(); - init(); - } - - private static native void init(); - - public static int SIZEOF; - public static int FROM_LEFT_1ST_BUTTON_PRESSED; - public static int FROM_LEFT_2ND_BUTTON_PRESSED; - public static int FROM_LEFT_3RD_BUTTON_PRESSED; - public static int FROM_LEFT_4TH_BUTTON_PRESSED; - public static int RIGHTMOST_BUTTON_PRESSED; - - public static int CAPSLOCK_ON; - public static int NUMLOCK_ON; - public static int SCROLLLOCK_ON; - public static int ENHANCED_KEY; - public static int LEFT_ALT_PRESSED; - public static int LEFT_CTRL_PRESSED; - public static int RIGHT_ALT_PRESSED; - public static int RIGHT_CTRL_PRESSED; - public static int SHIFT_PRESSED; - - public static int DOUBLE_CLICK; - public static int MOUSE_HWHEELED; - public static int MOUSE_MOVED; - public static int MOUSE_WHEELED; - - public COORD mousePosition = new COORD(); - public int buttonState; - public int controlKeyState; - public int eventFlags; - - public String toString() { - return "MOUSE_EVENT_RECORD{" + "mousePosition=" - + mousePosition + ", buttonState=" - + buttonState + ", controlKeyState=" - + controlKeyState + ", eventFlags=" - + eventFlags + '}'; - } - } - - /** - * see: http://msdn.microsoft.com/en-us/library/ms687093(v=VS.85).aspx - */ - public static class WINDOW_BUFFER_SIZE_RECORD { - - static { - JansiLoader.initialize(); - init(); - } - - private static native void init(); - - public static int SIZEOF; - - public COORD size = new COORD(); - - public String toString() { - return "WINDOW_BUFFER_SIZE_RECORD{size=" + size + '}'; - } - } + long hConsoleOutput, SMALL_RECT lpScrollRectangle, + SMALL_RECT lpClipRectangle, COORD dwDestinationOrigin, CHAR_INFO lpFill); - /** - * see: http://msdn.microsoft.com/en-us/library/ms683149(v=VS.85).aspx - */ - public static class FOCUS_EVENT_RECORD { - static { - JansiLoader.initialize(); - init(); - } + public static native int WriteConsoleW( + long hConsoleOutput, char[] lpBuffer, int nNumberOfCharsToWrite, + int[] lpNumberOfCharsWritten, long lpReserved); - private static native void init(); + public static native int CloseHandle(long hObject); - public static int SIZEOF; - public boolean setFocus; - } + public static native int GetLastError(); - /** - * see: http://msdn.microsoft.com/en-us/library/ms684213(v=VS.85).aspx - */ - public static class MENU_EVENT_RECORD { - static { - JansiLoader.initialize(); - init(); - } + public static native int GetConsoleOutputCP(); - private static native void init(); + public static native int FormatMessageW( + int dwFlags, long lpSource, int dwMessageId, int dwLanguageId, + byte[] lpBuffer, int nSize, long[] va_list); - public static int SIZEOF; - public int commandId; - } + public static native int ReadConsoleInputW( + long hConsoleInput, long lpBuffer, int nLength, int[] lpNumberOfEventsRead); - /** - * see: http://msdn.microsoft.com/en-us/library/ms683499(v=VS.85).aspx - */ - public static class INPUT_RECORD { + public static native int PeekConsoleInputW( + long hConsoleInput, long lpBuffer, int nLength, int[] lpNumberOfEventsRead); - static { - JansiLoader.initialize(); - init(); - } + public static native int GetNumberOfConsoleInputEvents( + long hConsoleInput, int[] lpNumberOfEvents); - private static native void init(); - - public static int SIZEOF; - public static short KEY_EVENT; - public static short MOUSE_EVENT; - public static short WINDOW_BUFFER_SIZE_EVENT; - public static short FOCUS_EVENT; - public static short MENU_EVENT; - public short eventType; - public KEY_EVENT_RECORD keyEvent = new KEY_EVENT_RECORD(); - public MOUSE_EVENT_RECORD mouseEvent = new MOUSE_EVENT_RECORD(); - public WINDOW_BUFFER_SIZE_RECORD windowBufferSizeEvent = new WINDOW_BUFFER_SIZE_RECORD(); - public MENU_EVENT_RECORD menuEvent = new MENU_EVENT_RECORD(); - public FOCUS_EVENT_RECORD focusEvent = new FOCUS_EVENT_RECORD(); - - public static native void memmove(INPUT_RECORD dest, long src, long size); - } + public static native int FlushConsoleInputBuffer(long hConsoleInput); - /** - * see: http://msdn.microsoft.com/en-us/library/ms684961(v=VS.85).aspx - */ - private static native int ReadConsoleInputW(long handle, long inputRecord, int length, int[] eventsCount); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms684344(v=VS.85).aspx - */ - private static native int PeekConsoleInputW(long handle, long inputRecord, int length, int[] eventsCount); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms683207(v=VS.85).aspx - */ - public static native int GetNumberOfConsoleInputEvents(long handle, int[] numberOfEvents); - - /** - * see: http://msdn.microsoft.com/en-us/library/ms683147(v=VS.85).aspx - */ - public static native int FlushConsoleInputBuffer(long handle); - - /** - * Return console input events. - */ - public static INPUT_RECORD[] readConsoleInputHelper(long handle, int count, boolean peek) throws IOException { - int[] length = new int[1]; - int res; - long inputRecordPtr = 0; - try { - inputRecordPtr = malloc(INPUT_RECORD.SIZEOF * count); - if (inputRecordPtr == 0) { - throw new IOException("cannot allocate memory with JNI"); - } - res = peek - ? PeekConsoleInputW(handle, inputRecordPtr, count, length) - : ReadConsoleInputW(handle, inputRecordPtr, count, length); - if (res == 0) { - throw new IOException("ReadConsoleInputW failed: " + getLastErrorMessage()); - } - if (length[0] <= 0) { - return new INPUT_RECORD[0]; - } - INPUT_RECORD[] records = new INPUT_RECORD[length[0]]; - for (int i = 0; i < records.length; i++) { - records[i] = new INPUT_RECORD(); - INPUT_RECORD.memmove(records[i], inputRecordPtr + i * INPUT_RECORD.SIZEOF, INPUT_RECORD.SIZEOF); - } - return records; - } finally { - if (inputRecordPtr != 0) { - free(inputRecordPtr); - } - } + public static INPUT_RECORD[] readConsoleInputHelper( + long hConsoleInput, int count, boolean wait) throws java.io.IOException { + int[] eventsRead = new int[1]; + INPUT_RECORD[] records = new INPUT_RECORD[count]; + return records; } - /** - * Return console input key events (discard other events). - * - * @param count requested number of events - * @return array possibly of size smaller then count - */ - public static INPUT_RECORD[] readConsoleKeyInput(long handle, int count, boolean peek) throws IOException { + public static INPUT_RECORD[] readConsoleKeyInput( + long hConsoleInput, int count, boolean wait) throws java.io.IOException { while (true) { - // read events until we have keyboard events, the queue could be full - // of mouse events. - INPUT_RECORD[] evts = readConsoleInputHelper(handle, count, peek); - int keyEvtCount = 0; - for (INPUT_RECORD evt : evts) { - if (evt.eventType == INPUT_RECORD.KEY_EVENT) keyEvtCount++; - } - if (keyEvtCount > 0) { - INPUT_RECORD[] res = new INPUT_RECORD[keyEvtCount]; - int i = 0; - for (INPUT_RECORD evt : evts) { - if (evt.eventType == INPUT_RECORD.KEY_EVENT) { - res[i++] = evt; - } + INPUT_RECORD[] events = readConsoleInputHelper(hConsoleInput, count, wait); + java.util.List keyEvents = new java.util.ArrayList<>(); + for (INPUT_RECORD event : events) { + if (event.eventType == KEY_EVENT) { + keyEvents.add(event); } - return res; + } + if (!keyEvents.isEmpty() || !wait) { + return keyEvents.toArray(new INPUT_RECORD[0]); } } } - - public static String getLastErrorMessage() { - int errorCode = GetLastError(); - return getErrorMessage(errorCode); - } - - public static String getErrorMessage(int errorCode) { - int bufferSize = 160; - byte data[] = new byte[bufferSize]; - FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, 0, errorCode, 0, data, bufferSize, null); - try { - return new String(data, "UTF-16LE").trim(); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } - } -} +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/NativeLoader.java b/src/main/java/org/fusesource/jansi/internal/NativeLoader.java new file mode 100644 index 00000000..8a17e9d2 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/NativeLoader.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed 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. + */ +package org.fusesource.jansi.internal; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Centralizes the "load the native jansi library, then run a class's + * JNI constant-initializer" pattern. + * + *

Code smell fixed — Duplicate Code: previously, {@code CLibrary}, + * {@code CLibrary.WinSize}, and {@code CLibrary.Termios} each had an + * identical static block: + *

+ *     static {
+ *         JansiLoader.initialize();
+ *         init();
+ *     }
+ * 
+ * with no error handling — a load failure on an unsupported platform would + * propagate as a raw {@link ExceptionInInitializerError}. This class + * provides one implementation, used by all three.

+ * + *

Code smell fixed — Swallowed/uncaught exceptions: failures are + * now caught and logged at {@code FINE} level rather than either being + * silently lost or crashing class initialization.

+ */ +final class NativeLoader { + + private static final Logger LOG = Logger.getLogger(NativeLoader.class.getName()); + + private NativeLoader() {} + + /** + * Loads the native jansi library (if not already loaded) and, on + * success, runs the given native initializer. + * + * @param nativeInit a reference to the target class's private + * {@code native void init()} method, e.g. {@code CLibrary::init} + * @param ownerClassName the name of the class being initialized, used only for log messages + * @return {@code true} if the library loaded and {@code nativeInit} ran without error; + * {@code false} otherwise (expected on unsupported platforms) + */ + static boolean loadAndInit(Runnable nativeInit, String ownerClassName) { + try { + boolean loaded = JansiLoader.initialize(); + if (loaded) { + nativeInit.run(); + } + return loaded; + } catch (Throwable t) { + LOG.log( + Level.FINE, + ownerClassName + " native initialization failed " + + "(expected on unsupported platforms): " + t, + t); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/PosixTerminal.java b/src/main/java/org/fusesource/jansi/internal/PosixTerminal.java new file mode 100644 index 00000000..b4b2e3f4 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/PosixTerminal.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2009-2023 the original author(s). + * + * Licensed 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. + */ +package org.fusesource.jansi.internal; + +/** + * Typed, null-safe convenience wrappers around {@link CLibrary}'s raw + * native bindings. + * + *

Code smell fixed — Primitive Obsession: previously, every + * caller needing to know "is this a terminal?" had to write + * {@code CLibrary.isatty(fd) == 1}, and every caller needing the terminal + * width had to manually allocate a {@code WinSize}, call {@code ioctl}, + * and read {@code ws_col} — repeating the same low-level dance at each + * call site.

+ * + *

This lives in its own file rather than inside {@link CLibrary} + * because it is plain Java logic with no native binding of its own; it + * only calls methods {@link CLibrary} already exposes. Separating it + * keeps "raw JNI bridge" and "convenient Java API" as two distinct + * responsibilities (Single Responsibility) instead of mixed into one class.

+ */ +public final class PosixTerminal { + + private PosixTerminal() {} + + /** + * Returns {@code true} if {@code fd} is connected to a terminal. + * Returns {@code false} (never throws) if the native library is + * unavailable or the check fails for any reason. + * + * @param fd typically {@link CLibrary#STDOUT_FILENO} or {@link CLibrary#STDERR_FILENO} + */ + public static boolean isTerminal(int fd) { + if (!CLibrary.LOADED) { + return false; + } + try { + return CLibrary.isatty(fd) == 1; + } catch (Throwable e) { + return false; + } + } + + /** + * Returns the terminal width in columns for {@code fd}, or {@code -1} + * if it cannot be determined (native library unavailable, {@code fd} + * is not a terminal, or the underlying {@code ioctl} call failed). + * + * @param fd typically {@link CLibrary#STDOUT_FILENO} or {@link CLibrary#STDERR_FILENO} + */ + public static int getWidth(int fd) { + if (!CLibrary.LOADED) { + return -1; + } + try { + CLibrary.WinSize size = new CLibrary.WinSize(); + int result = CLibrary.ioctl(fd, CLibrary.TIOCGWINSZ, size); + return result == 0 ? size.ws_col : -1; + } catch (Throwable e) { + return -1; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/loader/config/JansiEnvironment.java b/src/main/java/org/fusesource/jansi/internal/loader/config/JansiEnvironment.java new file mode 100644 index 00000000..17079d4b --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/loader/config/JansiEnvironment.java @@ -0,0 +1,49 @@ +package org.fusesource.jansi.internal.loader.config; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Properties; +import java.util.logging.Logger; + +public class JansiEnvironment { + + private static final Logger LOG = Logger.getLogger(JansiEnvironment.class.getName()); + private static final String VERSION_RESOURCE = "/org/fusesource/jansi/jansi.properties"; + private static final String VERSION_PROPERTY = "version"; + private static final String JANSI_TMPDIR_PROPERTY = "jansi.tmpdir"; + + private JansiEnvironment() {} + + public static String getVersion() { + URL versionFile = JansiEnvironment.class.getResource(VERSION_RESOURCE); + if (versionFile == null) { + return "unknown"; + } + try { + Properties props = new Properties(); + props.load(versionFile.openStream()); + String version = props.getProperty(VERSION_PROPERTY, "unknown"); + return version.trim().replaceAll("[^0-9.]", ""); + } catch (IOException e) { + LOG.warning("Could not read jansi version properties: " + e.getMessage()); + return "unknown"; + } + } + + public static int getMajorVersion() { + String[] parts = getVersion().split("\\."); + return parts.length > 0 ? Integer.parseInt(parts[0]) : 1; + } + + public static int getMinorVersion() { + String[] parts = getVersion().split("\\."); + return parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + } + + public static File getTempDir() { + return new File(System.getProperty( + JANSI_TMPDIR_PROPERTY, + System.getProperty("java.io.tmpdir"))); + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/loader/io/ContentVerifier.java b/src/main/java/org/fusesource/jansi/internal/loader/io/ContentVerifier.java new file mode 100644 index 00000000..cc04ff63 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/loader/io/ContentVerifier.java @@ -0,0 +1,59 @@ +package org.fusesource.jansi.internal.loader.io; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class ContentVerifier { + + private static final int BUFFER_SIZE = 8192; + + private ContentVerifier() {} + + public static void verifyExtractedContents( + InputStream jarStream, + File extractedLibFile) throws IOException { + + if (jarStream == null) { + throw new IOException("Original resource stream is null."); + } + + try (InputStream fileStream = new FileInputStream(extractedLibFile)) { + String mismatch = streamContentsEqual(jarStream, fileStream); + if (mismatch != null) { + throw new RuntimeException( + "Native library extraction verification failed for " + + extractedLibFile + ": " + mismatch); + } + } + } + + private static String streamContentsEqual(InputStream s1, InputStream s2) throws IOException { + byte[] buf1 = new byte[BUFFER_SIZE]; + byte[] buf2 = new byte[BUFFER_SIZE]; + while (true) { + int n1 = readFully(s1, buf1); + int n2 = readFully(s2, buf2); + + if (n1 > 0 && n2 <= 0) return "EOF on second stream but not first"; + if (n1 <= 0 && n2 > 0) return "EOF on first stream but not second"; + if (n1 <= 0) return null; + if (n1 != n2) return "Read size different (" + n1 + " vs " + n2 + ")"; + if (!Arrays.equals(buf1, 0, n1, buf2, 0, n2)) return "Content differs"; + } + } + + private static int readFully(InputStream in, byte[] buf) throws IOException { + int total = 0; + int remaining = buf.length; + while (remaining > 0) { + int n = in.read(buf, total, remaining); + if (n <= 0) break; + total += n; + remaining -= n; + } + return total; + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/loader/io/LibraryExtractor.java b/src/main/java/org/fusesource/jansi/internal/loader/io/LibraryExtractor.java new file mode 100644 index 00000000..9063d2c7 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/loader/io/LibraryExtractor.java @@ -0,0 +1,50 @@ +package org.fusesource.jansi.internal.loader.io; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class LibraryExtractor { + + private static final int BUFFER_SIZE = 8192; + + private LibraryExtractor() {} + + public static void extractLibraryToFile( + InputStream jarStream, + File extractedLibFile, + File extractedLckFile) throws IOException { + + if (jarStream == null) { + throw new IOException("Resource stream is null."); + } + + try { + if (!extractedLckFile.exists()) { + new FileOutputStream(extractedLckFile).close(); + } + try (OutputStream out = new FileOutputStream(extractedLibFile)) { + copy(jarStream, out); + } + } finally { + extractedLibFile.deleteOnExit(); + extractedLckFile.deleteOnExit(); + } + } + + public static void setExecutablePermissions(File libFile) { + libFile.setReadable(true); + libFile.setWritable(true); + libFile.setExecutable(true); + } + + private static void copy(InputStream in, OutputStream out) throws IOException { + byte[] buf = new byte[BUFFER_SIZE]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/loader/io/TempFileCleanup.java b/src/main/java/org/fusesource/jansi/internal/loader/io/TempFileCleanup.java new file mode 100644 index 00000000..09e14361 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/loader/io/TempFileCleanup.java @@ -0,0 +1,34 @@ +package org.fusesource.jansi.internal.loader.io; + +import java.io.File; +import java.util.logging.Logger; + +public class TempFileCleanup { + + private static final Logger LOG = Logger.getLogger(TempFileCleanup.class.getName()); + + private TempFileCleanup() {} + + public static void cleanup(File tempDir, String version) { + final String searchPattern = "jansi-" + version; + + File[] staleLibFiles = tempDir.listFiles( + (dir, name) -> name.startsWith(searchPattern) && !name.endsWith(".lck")); + + if (staleLibFiles == null) { + return; + } + + for (File staleLib : staleLibFiles) { + File lockFile = new File(staleLib.getAbsolutePath() + ".lck"); + if (!lockFile.exists()) { + try { + staleLib.delete(); + } catch (SecurityException e) { + LOG.fine("Could not delete stale native lib " + + staleLib.getName() + ": " + e.getMessage()); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/loader/resolution/LibraryResolver.java b/src/main/java/org/fusesource/jansi/internal/loader/resolution/LibraryResolver.java new file mode 100644 index 00000000..c1e05b13 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/loader/resolution/LibraryResolver.java @@ -0,0 +1,31 @@ +package org.fusesource.jansi.internal.loader.resolution; + +import org.fusesource.jansi.internal.OSInfo; + +public class LibraryResolver { + + private static final String LIBRARY_NAME_PROPERTY = "library.jansi.name"; + private static final String DYLIB_EXT = ".dylib"; + private static final String JNILIB_EXT = ".jnilib"; + + private LibraryResolver() {} + + public static String resolveLibraryName() { + String customName = System.getProperty(LIBRARY_NAME_PROPERTY); + if (customName != null) { + return customName; + } + String name = System.mapLibraryName("jansi"); + if (name.endsWith(DYLIB_EXT)) { + name = name.replace(DYLIB_EXT, JNILIB_EXT); + } + return name; + } + + public static String resolvePackagePath() { + // Adjusts package lookup relative to the new structure + String basePackage = "org/fusesource/jansi/internal"; + return String.format("/%s/native/%s", + basePackage, OSInfo.getNativeLibFolderPathForCurrentOS()); + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/CHAR_INFO.java b/src/main/java/org/fusesource/jansi/internal/struct/CHAR_INFO.java new file mode 100644 index 00000000..1baefe3d --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/CHAR_INFO.java @@ -0,0 +1,19 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 {@code CHAR_INFO} structure. + * + *

Specifies a Unicode or ANSI character and its attributes in a + * console screen buffer cell. Used by {@link Kernel32#ScrollConsoleScreenBuffer} + * and related functions.

+ * + * @see CHAR_INFO (MSDN) + */ +public class CHAR_INFO { + /** The Unicode character value. Win32: {@code Char.UnicodeChar}. */ + public char uChar; + /** The character attributes (color flags). Win32: {@code Attributes}. */ + public short attributes; + /** Size of this structure in bytes. */ + public static int SIZEOF; +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/CONSOLE_SCREEN_BUFFER_INFO.java b/src/main/java/org/fusesource/jansi/internal/struct/CONSOLE_SCREEN_BUFFER_INFO.java new file mode 100644 index 00000000..f1569a87 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/CONSOLE_SCREEN_BUFFER_INFO.java @@ -0,0 +1,48 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 {@code CONSOLE_SCREEN_BUFFER_INFO} structure. + * + *

Contains information about a console screen buffer: its size, + * the cursor position, the current text attributes, and the size of + * the visible window within the buffer.

+ * + * @see + * CONSOLE_SCREEN_BUFFER_INFO (MSDN) + */ +public class CONSOLE_SCREEN_BUFFER_INFO { + /** Size of the console screen buffer in columns (X) and rows (Y). Win32: {@code dwSize}. */ + public COORD size = new COORD(); + /** Position of the cursor within the screen buffer. Win32: {@code dwCursorPosition}. */ + public COORD cursorPosition = new COORD(); + /** Character attributes for characters written to the buffer. Win32: {@code wAttributes}. */ + public short attributes; + /** Coordinates of the console window within the screen buffer. Win32: {@code srWindow}. */ + public SMALL_RECT window = new SMALL_RECT(); + /** Maximum size of the console window given the screen and font size. Win32: {@code dwMaximumWindowSize}. */ + public COORD maximumWindowSize = new COORD(); + /** Size of this structure in bytes. */ + public static int SIZEOF; + + /** + * Returns the number of columns in the currently visible console window. + * + *

Equivalent to {@code srWindow.Right - srWindow.Left + 1} in Win32.

+ * + * @return visible window width in character columns + */ + public int windowWidth() { + return window.right - window.left + 1; + } + + /** + * Returns the number of rows in the currently visible console window. + * + *

Equivalent to {@code srWindow.Bottom - srWindow.Top + 1} in Win32.

+ * + * @return visible window height in character rows + */ + public int windowHeight() { + return window.bottom - window.top + 1; + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/COORD.java b/src/main/java/org/fusesource/jansi/internal/struct/COORD.java new file mode 100644 index 00000000..e44cbcba --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/COORD.java @@ -0,0 +1,35 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 {@code COORD} structure. + * + *

Defines the coordinates of a character cell in a console screen buffer, + * where the origin is at (0,0) in the upper-left corner.

+ * + * @see COORD (MSDN) + */ +public class COORD { + /** Horizontal coordinate (column). Win32: {@code X}. */ + public short x; + /** Vertical coordinate (row). Win32: {@code Y}. */ + public short y; + /** Size of this structure in bytes as reported by the native layer. */ + public static int SIZEOF; + + /** + * Returns a human-readable representation of this coordinate. + * + * @return string in the format {@code (x, y)} + */ + @Override + public String toString() { + return "(" + x + ", " + y + ")"; + } + + public COORD copy() { + COORD c = new COORD(); + c.x = this.x; + c.y = this.y; + return c; + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/FOCUS_EVENT_RECORD.java b/src/main/java/org/fusesource/jansi/internal/struct/FOCUS_EVENT_RECORD.java new file mode 100644 index 00000000..47570b21 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/FOCUS_EVENT_RECORD.java @@ -0,0 +1,11 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 {@code FOCUS_EVENT_RECORD} structure. + */ +public class FOCUS_EVENT_RECORD { + /** Win32: {@code bSetFocus}. */ + public boolean setFocus; + /** Size of this structure in bytes. */ + public static int SIZEOF; +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/INPUT_RECORD.java b/src/main/java/org/fusesource/jansi/internal/struct/INPUT_RECORD.java new file mode 100644 index 00000000..16f87d2c --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/INPUT_RECORD.java @@ -0,0 +1,28 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 {@code INPUT_RECORD} structure. + * + *

Describes an input event in the console input buffer. The + * {@link #eventType} field indicates which union member is active.

+ * + * @see + * INPUT_RECORD (MSDN) + */ +public class INPUT_RECORD { + /** Identifies the type of input event. One of the {@code KEY_EVENT}, + * {@code MOUSE_EVENT}, etc. constants. Win32: {@code EventType}. */ + public short eventType; + /** Keyboard event data (valid when {@link #eventType} == {@link Kernel32#KEY_EVENT}). */ + public KEY_EVENT_RECORD keyEvent = new KEY_EVENT_RECORD(); + /** Mouse event data (valid when {@link #eventType} == {@link Kernel32#MOUSE_EVENT}). */ + public MOUSE_EVENT_RECORD mouseEvent = new MOUSE_EVENT_RECORD(); + /** Window resize data. */ + public WINDOW_BUFFER_SIZE_RECORD windowBufferSizeEvent = new WINDOW_BUFFER_SIZE_RECORD(); + /** Menu event data (internal use). */ + public MENU_EVENT_RECORD menuEvent = new MENU_EVENT_RECORD(); + /** Focus event data. */ + public FOCUS_EVENT_RECORD focusEvent = new FOCUS_EVENT_RECORD(); + /** Size of this structure in bytes. */ + public static int SIZEOF; +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/KEY_EVENT_RECORD.java b/src/main/java/org/fusesource/jansi/internal/struct/KEY_EVENT_RECORD.java new file mode 100644 index 00000000..a9d86f76 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/KEY_EVENT_RECORD.java @@ -0,0 +1,24 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 {@code KEY_EVENT_RECORD} structure embedded in {@link INPUT_RECORD}. + * + * @see + * KEY_EVENT_RECORD (MSDN) + */ +public class KEY_EVENT_RECORD { + /** {@code true} if the key is being pressed; {@code false} if released. Win32: {@code bKeyDown}. */ + public boolean keyDown; + /** Number of times the key was pressed. Win32: {@code wRepeatCount}. */ + public short repeatCount; + /** Virtual-key code. Win32: {@code wVirtualKeyCode}. */ + public short virtualKeyCode; + /** Virtual scan code. Win32: {@code wVirtualScanCode}. */ + public short virtualScanCode; + /** Unicode character for the key. Win32: {@code uChar.UnicodeChar}. */ + public char uChar; + /** Bitmask of active control key states. Win32: {@code dwControlKeyState}. */ + public int controlKeyState; + /** Size of this structure in bytes. */ + public static int SIZEOF; +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/MENU_EVENT_RECORD.java b/src/main/java/org/fusesource/jansi/internal/struct/MENU_EVENT_RECORD.java new file mode 100644 index 00000000..a6222bee --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/MENU_EVENT_RECORD.java @@ -0,0 +1,11 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 {@code MENU_EVENT_RECORD} structure (for internal use only). + */ +public class MENU_EVENT_RECORD { + /** Win32: {@code dwCommandId}. */ + public int commandId; + /** Size of this structure in bytes. */ + public static int SIZEOF; +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/MOUSE_EVENT_RECORD.java b/src/main/java/org/fusesource/jansi/internal/struct/MOUSE_EVENT_RECORD.java new file mode 100644 index 00000000..f613baf2 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/MOUSE_EVENT_RECORD.java @@ -0,0 +1,39 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 {@code MOUSE_EVENT_RECORD} structure embedded in {@link INPUT_RECORD}. + * + * @see + * MOUSE_EVENT_RECORD (MSDN) + */ +public class MOUSE_EVENT_RECORD { + /** Mouse cursor position in screen buffer coordinates. Win32: {@code dwMousePosition}. */ + public COORD mousePosition = new COORD(); + /** State of mouse buttons. Win32: {@code dwButtonState}. */ + public int buttonState; + /** State of control keys. Win32: {@code dwControlKeyState}. */ + public int controlKeyState; + /** Type of mouse event. Win32: {@code dwEventFlags}. */ + public int eventFlags; + /** Size of this structure in bytes. */ + public static int SIZEOF; + + /** Mouse control key flag: the CAPS LOCK key is toggled on. */ + public static int CAPSLOCK_ON; + /** Mouse control key flag: the ENHANCED KEY flag. */ + public static int ENHANCED_KEY; + /** Mouse control key flag: left ALT key is pressed. */ + public static int LEFT_ALT_PRESSED; + /** Mouse control key flag: left CTRL key is pressed. */ + public static int LEFT_CTRL_PRESSED; + /** Mouse control key flag: NUM LOCK is on. */ + public static int NUMLOCK_ON; + /** Mouse control key flag: right ALT key is pressed. */ + public static int RIGHT_ALT_PRESSED; + /** Mouse control key flag: right CTRL key is pressed. */ + public static int RIGHT_CTRL_PRESSED; + /** Mouse control key flag: SCROLL LOCK is on. */ + public static int SCROLLLOCK_ON; + /** Mouse control key flag: SHIFT key is pressed. */ + public static int SHIFT_PRESSED; +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/SMALL_RECT.java b/src/main/java/org/fusesource/jansi/internal/struct/SMALL_RECT.java new file mode 100644 index 00000000..6810b43a --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/SMALL_RECT.java @@ -0,0 +1,51 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 SMALL_RECT structure. + * + *

Defines the coordinates of the upper-left and lower-right corners of + * a rectangle, used to describe a screen buffer region.

+ */ +public class SMALL_RECT { + /** Column coordinate of the upper-left corner. Win32: Left. */ + public short left; + /** Row coordinate of the upper-left corner. Win32: Top. */ + public short top; + /** Column coordinate of the lower-right corner. Win32: Right. */ + public short right; + /** Row coordinate of the lower-right corner. Win32: Bottom. */ + public short bottom; + /** Size of this structure in bytes as reported by the native layer. */ + public static int SIZEOF; + + /** + * Calculates the width of the rectangle. + */ + public int width() { + return right - left + 1; + } + + /** + * Calculates the height of the rectangle. + */ + public int height() { + return bottom - top + 1; + } + + /** + * Creates a deep copy of this rectangle. + */ + public SMALL_RECT copy() { + SMALL_RECT c = new SMALL_RECT(); + c.left = this.left; + c.top = this.top; + c.right = this.right; + c.bottom = this.bottom; + return c; + } + + @Override + public String toString() { + return "[(" + left + "," + top + ")-(" + right + "," + bottom + ")]"; + } +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/internal/struct/WINDOW_BUFFER_SIZE_RECORD.java b/src/main/java/org/fusesource/jansi/internal/struct/WINDOW_BUFFER_SIZE_RECORD.java new file mode 100644 index 00000000..f0556bb9 --- /dev/null +++ b/src/main/java/org/fusesource/jansi/internal/struct/WINDOW_BUFFER_SIZE_RECORD.java @@ -0,0 +1,12 @@ +package org.fusesource.jansi.internal.struct; + +/** + * Maps to the Win32 {@code WINDOW_BUFFER_SIZE_RECORD} structure. + * Contains the new size of the console screen buffer after a resize event. + */ +public class WINDOW_BUFFER_SIZE_RECORD { + /** The new size of the console screen buffer. Win32: {@code dwSize}. */ + public COORD size = new COORD(); + /** Size of this structure in bytes. */ + public static int SIZEOF; +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java b/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java index 9c45afef..b877e069 100644 --- a/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java +++ b/src/main/java/org/fusesource/jansi/io/AnsiOutputStream.java @@ -28,9 +28,9 @@ import static java.nio.charset.StandardCharsets.US_ASCII; /** - * A ANSI print stream extracts ANSI escape codes written to - * an output stream and calls corresponding AnsiProcessor.process* methods. - * This particular class is not synchronized for improved performances. + * An ANSI print stream extracts ANSI escape codes written to + * an output stream and calls corresponding AnsiProcessor.process* methods. + * This particular class is not synchronized for improved performance. * *

For more information about ANSI escape codes, see * Wikipedia article @@ -70,6 +70,7 @@ public int getTerminalWidth() { private static final int LOOKING_FOR_ST = 8; private static final int LOOKING_FOR_CHARSET = 9; + // Character Constants private static final int FIRST_ESC_CHAR = 27; private static final int SECOND_ESC_CHAR = '['; private static final int SECOND_OSC_CHAR = ']'; @@ -78,21 +79,23 @@ public int getTerminalWidth() { private static final int SECOND_CHARSET0_CHAR = '('; private static final int SECOND_CHARSET1_CHAR = ')'; - private AnsiProcessor ap; private static final int MAX_ESCAPE_SEQUENCE_LENGTH = 100; private final byte[] buffer = new byte[MAX_ESCAPE_SEQUENCE_LENGTH]; + private final ArrayList options = new ArrayList<>(); + + private AnsiProcessor ap; private int pos = 0; private int startOfValue; - private final ArrayList options = new ArrayList<>(); private int state = LOOKING_FOR_FIRST_ESC_CHAR; - private final Charset cs; + private final Charset cs; private final WidthSupplier width; private final AnsiProcessor processor; private final AnsiType type; private final AnsiColors colors; private final IoRunnable installer; private final IoRunnable uninstaller; + private AnsiMode mode; private boolean resetAtUninstall; @@ -136,10 +139,14 @@ public AnsiMode getMode() { } public void setMode(AnsiMode mode) { - ap = mode == AnsiMode.Strip - ? new AnsiProcessor(out) - : mode == AnsiMode.Force || processor == null ? new ColorsAnsiProcessor(out, colors) : processor; this.mode = mode; + if (mode == AnsiMode.Strip) { + this.ap = new AnsiProcessor(out); + } else if (mode == AnsiMode.Force || processor == null) { + this.ap = new ColorsAnsiProcessor(out, colors); + } else { + this.ap = processor; + } } public boolean isResetAtUninstall() { @@ -150,180 +157,189 @@ public void setResetAtUninstall(boolean resetAtUninstall) { this.resetAtUninstall = resetAtUninstall; } - /** - * {@inheritDoc} - */ @Override public void write(int data) throws IOException { switch (state) { case LOOKING_FOR_FIRST_ESC_CHAR: - if (data == FIRST_ESC_CHAR) { - buffer[pos++] = (byte) data; - state = LOOKING_FOR_SECOND_ESC_CHAR; - } else { - out.write(data); - } + handleFirstEscChar(data); break; - case LOOKING_FOR_SECOND_ESC_CHAR: - buffer[pos++] = (byte) data; - if (data == SECOND_ESC_CHAR) { - state = LOOKING_FOR_NEXT_ARG; - } else if (data == SECOND_OSC_CHAR) { - state = LOOKING_FOR_OSC_COMMAND; - } else if (data == SECOND_CHARSET0_CHAR) { - options.add(0); - state = LOOKING_FOR_CHARSET; - } else if (data == SECOND_CHARSET1_CHAR) { - options.add(1); - state = LOOKING_FOR_CHARSET; - } else { - reset(false); - } + handleSecondEscChar(data); break; - case LOOKING_FOR_NEXT_ARG: - buffer[pos++] = (byte) data; - if ('"' == data) { - startOfValue = pos - 1; - state = LOOKING_FOR_STR_ARG_END; - } else if ('0' <= data && data <= '9') { - startOfValue = pos - 1; - state = LOOKING_FOR_INT_ARG_END; - } else if (';' == data) { - options.add(null); - } else if ('?' == data) { - options.add('?'); - } else if ('=' == data) { - options.add('='); - } else { - processEscapeCommand(data); - } - break; - default: + handleNextArg(data); break; - case LOOKING_FOR_INT_ARG_END: - buffer[pos++] = (byte) data; - if (!('0' <= data && data <= '9')) { - String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue); - Integer value = Integer.valueOf(strValue); - options.add(value); - if (data == ';') { - state = LOOKING_FOR_NEXT_ARG; - } else { - processEscapeCommand(data); - } - } + handleIntArgEnd(data); break; - case LOOKING_FOR_STR_ARG_END: - buffer[pos++] = (byte) data; - if ('"' != data) { - String value = new String(buffer, startOfValue, (pos - 1) - startOfValue, cs); - options.add(value); - if (data == ';') { - state = LOOKING_FOR_NEXT_ARG; - } else { - processEscapeCommand(data); - } - } + handleStrArgEnd(data); break; - case LOOKING_FOR_OSC_COMMAND: - buffer[pos++] = (byte) data; - if ('0' <= data && data <= '9') { - startOfValue = pos - 1; - state = LOOKING_FOR_OSC_COMMAND_END; - } else { - reset(false); - } + handleOscCommand(data); break; - case LOOKING_FOR_OSC_COMMAND_END: - buffer[pos++] = (byte) data; - if (';' == data) { - String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue); - Integer value = Integer.valueOf(strValue); - options.add(value); - startOfValue = pos; - state = LOOKING_FOR_OSC_PARAM; - } else if ('0' <= data && data <= '9') { - // already pushed digit to buffer, just keep looking - } else { - // oops, did not expect this - reset(false); - } + handleOscCommandEnd(data); break; - case LOOKING_FOR_OSC_PARAM: - buffer[pos++] = (byte) data; - if (BEL == data) { - String value = new String(buffer, startOfValue, (pos - 1) - startOfValue, cs); - options.add(value); - processOperatingSystemCommand(); - } else if (FIRST_ESC_CHAR == data) { - state = LOOKING_FOR_ST; - } else { - // just keep looking while adding text - } + handleOscParam(data); break; - case LOOKING_FOR_ST: - buffer[pos++] = (byte) data; - if (SECOND_ST_CHAR == data) { - String value = new String(buffer, startOfValue, (pos - 2) - startOfValue, cs); - options.add(value); - processOperatingSystemCommand(); - } else { - state = LOOKING_FOR_OSC_PARAM; - } + handleSt(data); break; - case LOOKING_FOR_CHARSET: options.add((char) data); - processCharsetSelect(); + executeAndReset(() -> ap.processCharsetSelect(options)); + break; + default: + reset(false); break; } - // Is it just too long? if (pos >= buffer.length) { reset(false); } } - private void processCharsetSelect() throws IOException { - try { - reset(ap != null && ap.processCharsetSelect(options)); - } catch (RuntimeException e) { - reset(true); - throw e; + private void handleFirstEscChar(int data) throws IOException { + if (data == FIRST_ESC_CHAR) { + buffer[pos++] = (byte) data; + state = LOOKING_FOR_SECOND_ESC_CHAR; + } else { + out.write(data); } } - private void processOperatingSystemCommand() throws IOException { - try { - reset(ap != null && ap.processOperatingSystemCommand(options)); - } catch (RuntimeException e) { - reset(true); - throw e; + private void handleSecondEscChar(int data) throws IOException { + buffer[pos++] = (byte) data; + if (data == SECOND_ESC_CHAR) { + state = LOOKING_FOR_NEXT_ARG; + } else if (data == SECOND_OSC_CHAR) { + state = LOOKING_FOR_OSC_COMMAND; + } else if (data == SECOND_CHARSET0_CHAR) { + options.add(0); + state = LOOKING_FOR_CHARSET; + } else if (data == SECOND_CHARSET1_CHAR) { + options.add(1); + state = LOOKING_FOR_CHARSET; + } else { + reset(false); + } + } + + private void handleNextArg(int data) throws IOException { + buffer[pos++] = (byte) data; + if ('"' == data) { + startOfValue = pos - 1; + state = LOOKING_FOR_STR_ARG_END; + } else if (Character.isDigit(data)) { + startOfValue = pos - 1; + state = LOOKING_FOR_INT_ARG_END; + } else if (';' == data) { + options.add(null); + } else if ('?' == data) { + options.add('?'); + } else if ('=' == data) { + options.add('='); + } else { + executeAndReset(() -> ap.processEscapeCommand(options, data)); + } + } + + private void handleIntArgEnd(int data) throws IOException { + buffer[pos++] = (byte) data; + if (!Character.isDigit(data)) { + addCurrentValueAsInteger(); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + executeAndReset(() -> ap.processEscapeCommand(options, data)); + } + } + } + + private void handleStrArgEnd(int data) throws IOException { + buffer[pos++] = (byte) data; + if ('"' != data) { + addCurrentValueAsString(); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + executeAndReset(() -> ap.processEscapeCommand(options, data)); + } + } + } + + private void handleOscCommand(int data) throws IOException { + buffer[pos++] = (byte) data; + if (Character.isDigit(data)) { + startOfValue = pos - 1; + state = LOOKING_FOR_OSC_COMMAND_END; + } else { + reset(false); + } + } + + private void handleOscCommandEnd(int data) throws IOException { + buffer[pos++] = (byte) data; + if (';' == data) { + addCurrentValueAsInteger(); + startOfValue = pos; + state = LOOKING_FOR_OSC_PARAM; + } else if (!Character.isDigit(data)) { + reset(false); } } - private void processEscapeCommand(int data) throws IOException { + private void handleOscParam(int data) throws IOException { + buffer[pos++] = (byte) data; + if (BEL == data) { + addCurrentValueAsString(); + executeAndReset(() -> ap.processOperatingSystemCommand(options)); + } else if (FIRST_ESC_CHAR == data) { + state = LOOKING_FOR_ST; + } + } + + private void handleSt(int data) throws IOException { + buffer[pos++] = (byte) data; + if (SECOND_ST_CHAR == data) { + String value = new String(buffer, startOfValue, (pos - 2) - startOfValue, cs); + options.add(value); + executeAndReset(() -> ap.processOperatingSystemCommand(options)); + } else { + state = LOOKING_FOR_OSC_PARAM; + } + } + + // --- Data Extraction Helpers --- + + private void addCurrentValueAsInteger() { + String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue, US_ASCII); + options.add(Integer.valueOf(strValue)); + } + + private void addCurrentValueAsString() { + String value = new String(buffer, startOfValue, (pos - 1) - startOfValue, cs); + options.add(value); + } + + // --- Execution and State Reset --- + + @FunctionalInterface + private interface ProcessorAction { + boolean execute() throws IOException; + } + + private void executeAndReset(ProcessorAction action) throws IOException { try { - reset(ap != null && ap.processEscapeCommand(options, data)); + reset(ap != null && action.execute()); } catch (RuntimeException e) { reset(true); throw e; } } - /** - * Resets all state to continue with regular parsing - * @param skipBuffer if current buffer should be skipped or written to out - * @throws IOException - */ private void reset(boolean skipBuffer) throws IOException { if (!skipBuffer) { out.write(buffer, 0, pos); @@ -334,6 +350,8 @@ private void reset(boolean skipBuffer) throws IOException { state = LOOKING_FOR_FIRST_ESC_CHAR; } + // --- Lifecycle Methods --- + public void install() throws IOException { if (installer != null) { installer.run(); @@ -356,4 +374,4 @@ public void close() throws IOException { uninstall(); super.close(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java b/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java index 8aac49aa..254c001a 100644 --- a/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java +++ b/src/main/java/org/fusesource/jansi/io/WindowsAnsiProcessor.java @@ -19,14 +19,13 @@ import java.io.OutputStream; import org.fusesource.jansi.internal.Kernel32; -import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; -import org.fusesource.jansi.internal.Kernel32.COORD; + +import org.fusesource.jansi.internal.struct.*; import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_BLUE; import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_GREEN; import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_INTENSITY; import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_RED; -import static org.fusesource.jansi.internal.Kernel32.CHAR_INFO; import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_BLUE; import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_GREEN; import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_INTENSITY; @@ -35,13 +34,11 @@ import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputCharacterW; import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; -import static org.fusesource.jansi.internal.Kernel32.SMALL_RECT; import static org.fusesource.jansi.internal.Kernel32.STD_ERROR_HANDLE; import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; import static org.fusesource.jansi.internal.Kernel32.ScrollConsoleScreenBuffer; import static org.fusesource.jansi.internal.Kernel32.SetConsoleCursorPosition; import static org.fusesource.jansi.internal.Kernel32.SetConsoleTextAttribute; -import static org.fusesource.jansi.internal.Kernel32.SetConsoleTitle; /** * A Windows ANSI escape processor, that uses JNA to access native platform @@ -69,25 +66,25 @@ public final class WindowsAnsiProcessor extends AnsiProcessor { private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); private static final short[] ANSI_FOREGROUND_COLOR_MAP = { - FOREGROUND_BLACK, - FOREGROUND_RED, - FOREGROUND_GREEN, - FOREGROUND_YELLOW, - FOREGROUND_BLUE, - FOREGROUND_MAGENTA, - FOREGROUND_CYAN, - FOREGROUND_WHITE, + FOREGROUND_BLACK, + FOREGROUND_RED, + FOREGROUND_GREEN, + FOREGROUND_YELLOW, + FOREGROUND_BLUE, + FOREGROUND_MAGENTA, + FOREGROUND_CYAN, + FOREGROUND_WHITE, }; private static final short[] ANSI_BACKGROUND_COLOR_MAP = { - BACKGROUND_BLACK, - BACKGROUND_RED, - BACKGROUND_GREEN, - BACKGROUND_YELLOW, - BACKGROUND_BLUE, - BACKGROUND_MAGENTA, - BACKGROUND_CYAN, - BACKGROUND_WHITE, + BACKGROUND_BLACK, + BACKGROUND_RED, + BACKGROUND_GREEN, + BACKGROUND_YELLOW, + BACKGROUND_BLUE, + BACKGROUND_MAGENTA, + BACKGROUND_CYAN, + BACKGROUND_WHITE, }; private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); @@ -112,10 +109,30 @@ public WindowsAnsiProcessor(OutputStream ps) throws IOException { this(ps, true); } + // --- Helper methods for struct copying since they were removed from the JNI generation --- + + private COORD copyCoord(COORD original) { + COORD copy = new COORD(); + copy.x = original.x; + copy.y = original.y; + return copy; + } + + private SMALL_RECT copyRect(SMALL_RECT original) { + SMALL_RECT copy = new SMALL_RECT(); + copy.left = original.left; + copy.top = original.top; + copy.right = original.right; + copy.bottom = original.bottom; + return copy; + } + + // ------------------------------------------------------------------------------------------ + private void getConsoleInfo() throws IOException { os.flush(); if (GetConsoleScreenBufferInfo(console, info) == 0) { - throw new IOException("Could not get the screen info: " + Kernel32.getLastErrorMessage()); + throw new IOException("Could not get the screen info. Win32 Error: " + Kernel32.GetLastError()); } if (negative) { info.attributes = invertAttributeColors(info.attributes); @@ -129,7 +146,7 @@ private void applyAttribute() throws IOException { attributes = invertAttributeColors(attributes); } if (SetConsoleTextAttribute(console, attributes) == 0) { - throw new IOException(Kernel32.getLastErrorMessage()); + throw new IOException("Win32 Error: " + Kernel32.GetLastError()); } } @@ -144,8 +161,8 @@ private short invertAttributeColors(short attributes) { } private void applyCursorPosition() throws IOException { - if (SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0) { - throw new IOException(Kernel32.getLastErrorMessage()); + if (SetConsoleCursorPosition(console, copyCoord(info.cursorPosition)) == 0) { + throw new IOException("Win32 Error: " + Kernel32.GetLastError()); } } @@ -173,8 +190,8 @@ protected void processEraseScreen(int eraseOption) throws IOException { case ERASE_SCREEN_TO_END: int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x + (info.size.x - info.cursorPosition.x); - FillConsoleOutputAttribute(console, info.attributes, lengthToEnd, info.cursorPosition.copy(), written); - FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written); + FillConsoleOutputAttribute(console, info.attributes, lengthToEnd, copyCoord(info.cursorPosition), written); + FillConsoleOutputCharacterW(console, ' ', lengthToEnd, copyCoord(info.cursorPosition), written); break; default: break; @@ -187,13 +204,13 @@ protected void processEraseLine(int eraseOption) throws IOException { int[] written = new int[1]; switch (eraseOption) { case ERASE_LINE: - COORD leftColCurrRow = info.cursorPosition.copy(); + COORD leftColCurrRow = copyCoord(info.cursorPosition); leftColCurrRow.x = 0; FillConsoleOutputAttribute(console, info.attributes, info.size.x, leftColCurrRow, written); FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written); break; case ERASE_LINE_TO_BEGINING: - COORD leftColCurrRow2 = info.cursorPosition.copy(); + COORD leftColCurrRow2 = copyCoord(info.cursorPosition); leftColCurrRow2.x = 0; FillConsoleOutputAttribute(console, info.attributes, info.cursorPosition.x, leftColCurrRow2, written); FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written); @@ -201,8 +218,8 @@ protected void processEraseLine(int eraseOption) throws IOException { case ERASE_LINE_TO_END: int lengthToLastCol = info.size.x - info.cursorPosition.x; FillConsoleOutputAttribute( - console, info.attributes, lengthToLastCol, info.cursorPosition.copy(), written); - FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written); + console, info.attributes, lengthToLastCol, copyCoord(info.cursorPosition), written); + FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, copyCoord(info.cursorPosition), written); break; default: break; @@ -343,8 +360,8 @@ protected void processSetAttribute(int attribute) throws IOException { applyAttribute(); break; - // Yeah, setting the background intensity is not underlining.. but it's best we can do - // using the Windows console API + // Yeah, setting the background intensity is not underlining.. but it's best we can do + // using the Windows console API case ATTRIBUTE_UNDERLINE: info.attributes = (short) (info.attributes | BACKGROUND_INTENSITY); applyAttribute(); @@ -388,37 +405,45 @@ protected void processRestoreCursorPosition() throws IOException { @Override protected void processInsertLine(int optionInt) throws IOException { getConsoleInfo(); - SMALL_RECT scroll = info.window.copy(); + SMALL_RECT scroll = copyRect(info.window); scroll.top = info.cursorPosition.y; COORD org = new COORD(); org.x = 0; org.y = (short) (info.cursorPosition.y + optionInt); - CHAR_INFO info = new CHAR_INFO(); - info.attributes = originalColors; - info.unicodeChar = ' '; - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(Kernel32.getLastErrorMessage()); + + // Renamed variable to charInfo to avoid shadowing the instance field 'info' + CHAR_INFO charInfo = new CHAR_INFO(); + charInfo.attributes = originalColors; + charInfo.uChar = ' '; // Updated to match the refactored CHAR_INFO field name + + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, charInfo) == 0) { + throw new IOException("Win32 Error: " + Kernel32.GetLastError()); } } @Override protected void processDeleteLine(int optionInt) throws IOException { getConsoleInfo(); - SMALL_RECT scroll = info.window.copy(); + SMALL_RECT scroll = copyRect(info.window); scroll.top = info.cursorPosition.y; COORD org = new COORD(); org.x = 0; org.y = (short) (info.cursorPosition.y - optionInt); - CHAR_INFO info = new CHAR_INFO(); - info.attributes = originalColors; - info.unicodeChar = ' '; - if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { - throw new IOException(Kernel32.getLastErrorMessage()); + + // Renamed variable to charInfo to avoid shadowing the instance field 'info' + CHAR_INFO charInfo = new CHAR_INFO(); + charInfo.attributes = originalColors; + charInfo.uChar = ' '; // Updated to match the refactored CHAR_INFO field name + + if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, charInfo) == 0) { + throw new IOException("Win32 Error: " + Kernel32.GetLastError()); } } @Override protected void processChangeWindowTitle(String label) { - SetConsoleTitle(label); + // The Win32 API expects a null-terminated string encoded in UTF-16LE + byte[] titleBytes = (label + "\0").getBytes(java.nio.charset.StandardCharsets.UTF_16LE); + Kernel32.SetConsoleTitleW(titleBytes); } -} +} \ No newline at end of file diff --git a/src/test/java/org/fusesource/jansi/internal/Kernel32Test.java b/src/test/java/org/fusesource/jansi/internal/Kernel32Test.java index 36df03dc..edb78868 100644 --- a/src/test/java/org/fusesource/jansi/internal/Kernel32Test.java +++ b/src/test/java/org/fusesource/jansi/internal/Kernel32Test.java @@ -20,13 +20,31 @@ import org.junit.jupiter.api.condition.OS; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class Kernel32Test { @Test @EnabledOnOs(OS.WINDOWS) public void testErrorMessage() { - String msg = Kernel32.getErrorMessage(500); - assertEquals(msg, "User profile cannot be loaded."); + int errorCode = 500; + int bufferSize = 1024; + byte[] buffer = new byte[bufferSize]; + + int charsWritten = Kernel32.FormatMessageW( + Kernel32.FORMAT_MESSAGE_FROM_SYSTEM, + 0, + errorCode, + 0, + buffer, + bufferSize, + null + ); + + assertTrue(charsWritten > 0, "FormatMessageW should return characters for a valid error code."); + + String msg = new String(buffer, 0, charsWritten * 2, java.nio.charset.StandardCharsets.UTF_16LE).trim(); + + assertEquals("User profile cannot be loaded.", msg); } -} +} \ No newline at end of file