ColorPalette.java
/*
* Copyright (c) 2002-2020, the original author(s).
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* https://opensource.org/licenses/BSD-3-Clause
*/
package org.jline.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);
}
}