Skip to content

Commit

Permalink
Support terminal palette, fixes #620
Browse files Browse the repository at this point in the history
  • Loading branch information
gnodet committed Dec 10, 2020
1 parent e2b6f97 commit 8ff1e1d
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 10 deletions.
7 changes: 7 additions & 0 deletions terminal/src/main/java/org/jline/terminal/Terminal.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.function.IntSupplier;

import org.jline.terminal.impl.NativeSignalHandler;
import org.jline.utils.ColorPalette;
import org.jline.utils.InfoCmp.Capability;
import org.jline.utils.NonBlockingReader;

Expand Down Expand Up @@ -328,4 +329,10 @@ enum MouseTracking {
* @return <code>true</code> if focus tracking is supported
*/
boolean trackFocus(boolean tracking);

/**
* Color support
*/
ColorPalette getPalette();

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.jline.terminal.Cursor;
import org.jline.terminal.MouseEvent;
import org.jline.terminal.Terminal;
import org.jline.utils.ColorPalette;
import org.jline.utils.Curses;
import org.jline.utils.InfoCmp;
import org.jline.utils.InfoCmp.Capability;
Expand All @@ -42,6 +43,7 @@ public abstract class AbstractTerminal implements Terminal {
protected final Set<Capability> bools = new HashSet<>();
protected final Map<Capability, Integer> ints = new HashMap<>();
protected final Map<Capability, String> strings = new HashMap<>();
protected final ColorPalette palette = new ColorPalette(this);
protected Status status;
protected Runnable onClose;

Expand Down Expand Up @@ -283,4 +285,8 @@ public boolean paused() {
return false;
}

@Override
public ColorPalette getPalette() {
return palette;
}
}
29 changes: 19 additions & 10 deletions terminal/src/main/java/org/jline/utils/AttributedCharSequence.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public String toAnsi(Terminal terminal) {
}
int colors = 256;
ForceMode forceMode = ForceMode.None;
ColorPalette palette = null;
String alternateIn = null, alternateOut = null;
if (terminal != null) {
Integer max_colors = terminal.getNumericCapability(Capability.max_colors);
Expand All @@ -78,12 +79,13 @@ public String toAnsi(Terminal terminal) {
|| AbstractWindowsTerminal.TYPE_WINDOWS_CONEMU.equals(terminal.getType())) {
forceMode = ForceMode.Force256Colors;
}
palette = terminal.getPalette();
if (!DISABLE_ALTERNATE_CHARSET) {
alternateIn = Curses.tputs(terminal.getStringCapability(Capability.enter_alt_charset_mode));
alternateOut = Curses.tputs(terminal.getStringCapability(Capability.exit_alt_charset_mode));
}
}
return toAnsi(colors, forceMode, alternateIn, alternateOut);
return toAnsi(colors, forceMode, palette, alternateIn, alternateOut);
}

@Deprecated
Expand All @@ -93,19 +95,26 @@ public String toAnsi(int colors, boolean force256colors) {

@Deprecated
public String toAnsi(int colors, boolean force256colors, String altIn, String altOut) {
return toAnsi(colors, force256colors ? ForceMode.Force256Colors : ForceMode.None, altIn, altOut);
return toAnsi(colors, force256colors ? ForceMode.Force256Colors : ForceMode.None, null, altIn, altOut);
}

public String toAnsi(int colors, ForceMode force) {
return toAnsi(colors, force, null, null);
return toAnsi(colors, force, null, null, null);
}

public String toAnsi(int colors, ForceMode force, String altIn, String altOut) {
public String toAnsi(int colors, ForceMode force, ColorPalette palette) {
return toAnsi(colors, force, palette, null, null);
}

public String toAnsi(int colors, ForceMode force, ColorPalette palette, String altIn, String altOut) {
StringBuilder sb = new StringBuilder();
long style = 0;
long foreground = 0;
long background = 0;
boolean alt = false;
if (palette == null) {
palette = ColorPalette.DEFAULT;
}
for (int i = 0; i < length(); i++) {
char c = charAt(i);
if (altIn != null && altOut != null) {
Expand Down Expand Up @@ -168,14 +177,14 @@ public String toAnsi(int colors, ForceMode force, String altIn, String altOut) {
if (colors == TRUE_COLORS) {
first = attr(sb, "38;2;" + r + ";" + g + ";" + b, first);
} else {
rounded = Colors.roundRgbColor(r, g, b, colors);
rounded = palette.round(r, g, b);
}
} else if ((fg & F_FOREGROUND_IND) != 0) {
rounded = Colors.roundColor((int)(fg >> FG_COLOR_EXP) & 0xFF, colors);
rounded = palette.round((int)(fg >> FG_COLOR_EXP) & 0xFF);
}
if (rounded >= 0) {
if (colors == TRUE_COLORS && force == ForceMode.ForceTrueColors) {
int col = Colors.rgbColor(rounded);
int col = palette.getColor(rounded);
int r = (col >> 16) & 0xFF;
int g = (col >> 8) & 0xFF;
int b = col & 0xFF;
Expand Down Expand Up @@ -207,14 +216,14 @@ public String toAnsi(int colors, ForceMode force, String altIn, String altOut) {
if (colors == TRUE_COLORS) {
first = attr(sb, "48;2;" + r + ";" + g + ";" + b, first);
} else {
rounded = Colors.roundRgbColor(r, g, b, colors);
rounded = palette.round(r, g, b);
}
} else if ((bg & F_BACKGROUND_IND) != 0) {
rounded = Colors.roundColor((int)(bg >> BG_COLOR_EXP) & 0xFF, colors);
rounded = palette.round((int)(bg >> BG_COLOR_EXP) & 0xFF);
}
if (rounded >= 0) {
if (colors == TRUE_COLORS && force == ForceMode.ForceTrueColors) {
int col = Colors.rgbColor(rounded);
int col = palette.getColor(rounded);
int r = (col >> 16) & 0xFF;
int g = (col >> 8) & 0xFF;
int b = col & 0xFF;
Expand Down
262 changes: 262 additions & 0 deletions terminal/src/main/java/org/jline/utils/ColorPalette.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
/*
* Copyright (c) 2002-2020, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* https://opensource.org/licenses/BSD-3-Clause
*/
package org.jline.utils;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.jline.terminal.Terminal;

/**
* Color palette
*/
public class ColorPalette {

public static final String XTERM_INITC = "\\E]4;%p1%d;rgb\\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\\E\\\\";

public static final ColorPalette DEFAULT = new ColorPalette();

private final Terminal terminal;
private String distanceName;
private Colors.Distance distance;
private boolean osc4;
private int[] palette;

public ColorPalette() {
this.terminal = null;
this.distanceName = null;
this.palette = Colors.DEFAULT_COLORS_256;
}

public ColorPalette(Terminal terminal) throws IOException {
this(terminal, null);
}

public ColorPalette(Terminal terminal, String distance) throws IOException {
this.terminal = terminal;
this.distanceName = distance;
loadPalette(false);
}

/**
* Get the name of the distance to use for rounding colors.
* @return the name of the color distance
*/
public String getDistanceName() {
return distanceName;
}

/**
* Set the name of the color distance to use when rounding RGB colors to the palette.
* @param name the name of the color distance
*/
public void setDistance(String name) {
this.distanceName = name;
}

/**
* Check if the terminal has the capability to change colors.
* @return <code>true</code> if the terminal can change colors
*/
public boolean canChange() {
return terminal != null && terminal.getBooleanCapability(InfoCmp.Capability.can_change);
}

/**
* Load the palette from the terminal.
* If the palette has already been loaded, subsequent calls will simply return <code>true</code>.
*
* @return <code>true</code> if the palette has been successfully loaded.
* @throws IOException
*/
public boolean loadPalette() throws IOException {
if (!osc4) {
loadPalette(true);
}
return osc4;
}

protected void loadPalette(boolean doLoad) throws IOException {
if (terminal != null) {
int[] pal = doLoad ? doLoad(terminal) : null;
if (pal != null) {
this.palette = pal;
this.osc4 = true;
} else {
Integer cols = terminal.getNumericCapability(InfoCmp.Capability.max_colors);
if (cols != null) {
if (cols == Colors.DEFAULT_COLORS_88.length) {
this.palette = Colors.DEFAULT_COLORS_88;
} else {
this.palette = Arrays.copyOf(Colors.DEFAULT_COLORS_256, Math.min(cols, 256));
}
} else {
this.palette = Arrays.copyOf(Colors.DEFAULT_COLORS_256, 256);
}
this.osc4 = false;
}
} else {
this.palette = Colors.DEFAULT_COLORS_256;
this.osc4 = false;
}
}

/**
* Get the palette length
* @return the palette length
*/
public int getLength() {
return this.palette.length;
}

/**
* Get a specific color in the palette
* @param index the index of the color
* @return the color at the given index
*/
public int getColor(int index) {
return palette[index];
}

/**
* Change the color of the palette
* @param index the index of the color
* @param color the new color value
*/
public void setColor(int index, int color) {
palette[index] = color;
if (canChange()) {
String initc = terminal.getStringCapability(InfoCmp.Capability.initialize_color);
if (initc != null || osc4) {
// initc expects color in 0..1000 range
int r = (((color >> 16) & 0xFF) * 1000) / 255 + 1;
int g = (((color >> 8) & 0xFF) * 1000) / 255 + 1;
int b = ((color & 0xFF) * 1000) / 255 + 1;
if (initc == null) {
// This is the xterm version
initc = XTERM_INITC;
}
Curses.tputs(terminal.writer(), initc, index, r, g, b);
terminal.writer().flush();
}
}
}

public boolean isReal() {
return osc4;
}

public int round(int r, int g, int b) {
return Colors.roundColor((r << 16) + (g << 8) + b, palette, palette.length, getDist());
}

public int round(int col) {
if (col >= palette.length) {
col = Colors.roundColor(DEFAULT.getColor(col), palette, palette.length, getDist());
}
return col;
}

protected Colors.Distance getDist() {
if (distance == null) {
distance = Colors.getDistance(distanceName);
}
return distance;
}

private static int[] doLoad(Terminal terminal) throws IOException {
PrintWriter writer = terminal.writer();
NonBlockingReader reader = terminal.reader();

int[] palette = new int[256];
for (int i = 0; i < 16; i++) {
StringBuilder req = new StringBuilder(1024);
req.append("\033]4");
for (int j = 0; j < 16; j++) {
req.append(';').append(i * 16 + j).append(";?");
}
req.append("\033\\");
writer.write(req.toString());
writer.flush();

boolean black = true;
for (int j = 0; j < 16; j++) {
if (reader.peek(50) < 0) {
break;
}
if (reader.read(10) != '\033'
|| reader.read(10) != ']'
|| reader.read(10) != '4'
|| reader.read(10) != ';') {
return null;
}
int idx = 0;
int c;
while (true) {
c = reader.read(10);
if (c >= '0' && c <= '9') {
idx = idx * 10 + (c - '0');
} else if (c == ';') {
break;
} else {
return null;
}
}
if (idx > 255) {
return null;
}
if (reader.read(10) != 'r'
|| reader.read(10) != 'g'
|| reader.read(10) != 'b'
|| reader.read(10) != ':') {
return null;
}
StringBuilder sb = new StringBuilder(16);
List<String> rgb = new ArrayList<>();
while (true) {
c = reader.read(10);
if (c == '\007') {
rgb.add(sb.toString());
break;
} else if (c == '\033') {
c = reader.read(10);
if (c == '\\') {
rgb.add(sb.toString());
break;
} else {
return null;
}
} else if (c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
sb.append((char) c);
} else if (c == '/') {
rgb.add(sb.toString());
sb.setLength(0);
}
}
if (rgb.size() != 3) {
return null;
}
double r = Integer.parseInt(rgb.get(0), 16) / ((1 << (4 * rgb.get(0).length())) - 1.0);
double g = Integer.parseInt(rgb.get(1), 16) / ((1 << (4 * rgb.get(1).length())) - 1.0);
double b = Integer.parseInt(rgb.get(2), 16) / ((1 << (4 * rgb.get(2).length())) - 1.0);
palette[idx] = (int)((Math.round(r * 255) << 16) + (Math.round(g * 255) << 8) + Math.round(b * 255));
black &= palette[idx] == 0;
}
if (black) {
break;
}
}
int max = 256;
while (max > 0 && palette[--max] == 0);
return Arrays.copyOfRange(palette, 0, max + 1);
}
}

0 comments on commit 8ff1e1d

Please sign in to comment.