X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fbackend%2FECMA48Terminal.java;h=84f6528c5a348832642bc21315dea8b1ddbb5bbc;hb=615a0d99fd0aa4437116dd083147f9150d5e6527;hp=f23f6f447864a927e4dc172ee7ec6274834b4c69;hpb=fe0770f988e64fc0ccafd3d3b086b4a0eb559d3b;p=fanfix.git diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index f23f6f4..84f6528 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -40,7 +40,6 @@ import java.io.PrintWriter; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.LinkedList; @@ -57,8 +56,29 @@ import static jexer.TKeypress.*; * This class reads keystrokes and mouse events and emits output to ANSI * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc. */ -public final class ECMA48Terminal extends LogicalScreen - implements TerminalReader, Runnable { +public class ECMA48Terminal extends LogicalScreen + implements TerminalReader, Runnable { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * States in the input parser. + */ + private enum ParseState { + GROUND, + ESCAPE, + ESCAPE_INTERMEDIATE, + CSI_ENTRY, + CSI_PARAM, + MOUSE, + MOUSE_SGR, + } + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Emit debugging to stderr. @@ -66,8 +86,9 @@ public final class ECMA48Terminal extends LogicalScreen private boolean debugToStderr = false; /** - * If true, emit T.416-style RGB colors. This is a) expensive in - * bandwidth, and b) potentially terrible looking for non-xterms. + * If true, emit T.416-style RGB colors for normal system colors. This + * is a) expensive in bandwidth, and b) potentially terrible looking for + * non-xterms. */ private static boolean doRgbColor = false; @@ -76,15 +97,6 @@ public final class ECMA48Terminal extends LogicalScreen */ private SessionInfo sessionInfo; - /** - * Getter for sessionInfo. - * - * @return the SessionInfo - */ - public SessionInfo getSessionInfo() { - return sessionInfo; - } - /** * The event queue, filled up by a thread reading on input. */ @@ -104,20 +116,7 @@ public final class ECMA48Terminal extends LogicalScreen * Parameters being collected. E.g. if the string is \033[1;3m, then * params[0] will be 1 and params[1] will be 3. */ - private ArrayList params; - - /** - * States in the input parser. - */ - private enum ParseState { - GROUND, - ESCAPE, - ESCAPE_INTERMEDIATE, - CSI_ENTRY, - CSI_PARAM, - MOUSE, - MOUSE_SGR, - } + private List params; /** * Current parsing state. @@ -195,100 +194,39 @@ public final class ECMA48Terminal extends LogicalScreen */ private Object listener; - /** - * Set listener to a different Object. - * - * @param listener the new listening object that run() wakes up on new - * input - */ - public void setListener(final Object listener) { - this.listener = listener; - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * Get the output writer. - * - * @return the Writer - */ - public PrintWriter getOutput() { - return output; - } - - /** - * Check if there are events in the queue. + * Constructor sets up state for getEvent(). * - * @return if true, getEvents() has something to return to the backend + * @param listener the object this backend needs to wake up when new + * input comes in + * @param input an InputStream connected to the remote user, or null for + * System.in. If System.in is used, then on non-Windows systems it will + * be put in raw mode; shutdown() will (blindly!) put System.in in cooked + * mode. input is always converted to a Reader with UTF-8 encoding. + * @param output an OutputStream connected to the remote user, or null + * for System.out. output is always converted to a Writer with UTF-8 + * encoding. + * @param windowWidth the number of text columns to start with + * @param windowHeight the number of text rows to start with + * @throws UnsupportedEncodingException if an exception is thrown when + * creating the InputStreamReader */ - public boolean hasEvents() { - synchronized (eventQueue) { - return (eventQueue.size() > 0); - } - } + public ECMA48Terminal(final Object listener, final InputStream input, + final OutputStream output, final int windowWidth, + final int windowHeight) throws UnsupportedEncodingException { - /** - * Call 'stty' to set cooked mode. - * - *

Actually executes '/bin/sh -c stty sane cooked < /dev/tty' - */ - private void sttyCooked() { - doStty(false); - } + this(listener, input, output); - /** - * Call 'stty' to set raw mode. - * - *

Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip - * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten - * -parenb cs8 min 1 < /dev/tty' - */ - private void sttyRaw() { - doStty(true); - } - - /** - * Call 'stty' to set raw or cooked mode. - * - * @param mode if true, set raw mode, otherwise set cooked mode - */ - private void doStty(final boolean mode) { - String [] cmdRaw = { - "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty" - }; - String [] cmdCooked = { - "/bin/sh", "-c", "stty sane cooked < /dev/tty" - }; - try { - Process process; - if (mode) { - process = Runtime.getRuntime().exec(cmdRaw); - } else { - process = Runtime.getRuntime().exec(cmdCooked); - } - BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); - String line = in.readLine(); - if ((line != null) && (line.length() > 0)) { - System.err.println("WEIRD?! Normal output from stty: " + line); - } - while (true) { - BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8")); - line = err.readLine(); - if ((line != null) && (line.length() > 0)) { - System.err.println("Error output from stty: " + line); - } - try { - process.waitFor(); - break; - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - int rc = process.exitValue(); - if (rc != 0) { - System.err.println("stty returned error code: " + rc); - } - } catch (IOException e) { - e.printStackTrace(); - } + // Send dtterm/xterm sequences, which will probably not work because + // allowWindowOps is defaulted to false. + String resizeString = String.format("\033[8;%d;%dt", windowHeight, + windowWidth); + this.output.write(resizeString); + this.output.flush(); } /** @@ -487,6 +425,73 @@ public final class ECMA48Terminal extends LogicalScreen this(listener, input, reader, writer, false); } + // ------------------------------------------------------------------------ + // LogicalScreen ---------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Set the window title. + * + * @param title the new title + */ + @Override + public void setTitle(final String title) { + output.write(getSetTitleString(title)); + flush(); + } + + /** + * Push the logical screen to the physical device. + */ + @Override + public void flushPhysical() { + String result = flushString(); + if ((cursorVisible) + && (cursorY >= 0) + && (cursorX >= 0) + && (cursorY <= height - 1) + && (cursorX <= width - 1) + ) { + result += cursor(true); + result += gotoXY(cursorX, cursorY); + } else { + result += cursor(false); + } + output.write(result); + flush(); + } + + // ------------------------------------------------------------------------ + // TerminalReader --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Check if there are events in the queue. + * + * @return if true, getEvents() has something to return to the backend + */ + public boolean hasEvents() { + synchronized (eventQueue) { + return (eventQueue.size() > 0); + } + } + + /** + * Return any events in the IO queue. + * + * @param queue list to append new events to + */ + public void getEvents(final List queue) { + synchronized (eventQueue) { + if (eventQueue.size() > 0) { + synchronized (queue) { + queue.addAll(eventQueue); + } + eventQueue.clear(); + } + } + } + /** * Restore terminal to normal state. */ @@ -502,9 +507,12 @@ public final class ECMA48Terminal extends LogicalScreen e.printStackTrace(); } - // Disable mouse reporting and show cursor - output.printf("%s%s%s", mouse(false), cursor(true), normal()); - output.flush(); + // Disable mouse reporting and show cursor. Defensive null check + // here in case closeTerminal() is called twice. + if (output != null) { + output.printf("%s%s%s", mouse(false), cursor(true), normal()); + output.flush(); + } if (setRawMode) { sttyCooked(); @@ -522,9 +530,198 @@ public final class ECMA48Terminal extends LogicalScreen output.close(); output = null; } - } catch (IOException e) { - e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * Set listener to a different Object. + * + * @param listener the new listening object that run() wakes up on new + * input + */ + public void setListener(final Object listener) { + this.listener = listener; + } + + // ------------------------------------------------------------------------ + // Runnable --------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Read function runs on a separate thread. + */ + public void run() { + boolean done = false; + // available() will often return > 1, so we need to read in chunks to + // stay caught up. + char [] readBuffer = new char[128]; + List events = new LinkedList(); + + while (!done && !stopReaderThread) { + try { + // We assume that if inputStream has bytes available, then + // input won't block on read(). + int n = inputStream.available(); + + /* + System.err.printf("inputStream.available(): %d\n", n); + System.err.flush(); + */ + + if (n > 0) { + if (readBuffer.length < n) { + // The buffer wasn't big enough, make it huger + readBuffer = new char[readBuffer.length * 2]; + } + + // System.err.printf("BEFORE read()\n"); System.err.flush(); + + int rc = input.read(readBuffer, 0, readBuffer.length); + + /* + System.err.printf("AFTER read() %d\n", rc); + System.err.flush(); + */ + + if (rc == -1) { + // This is EOF + done = true; + } else { + for (int i = 0; i < rc; i++) { + int ch = readBuffer[i]; + processChar(events, (char)ch); + } + getIdleEvents(events); + if (events.size() > 0) { + // Add to the queue for the backend thread to + // be able to obtain. + synchronized (eventQueue) { + eventQueue.addAll(events); + } + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } + } + events.clear(); + } + } + } else { + getIdleEvents(events); + if (events.size() > 0) { + synchronized (eventQueue) { + eventQueue.addAll(events); + } + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } + } + events.clear(); + } + + // Wait 20 millis for more data + Thread.sleep(20); + } + // System.err.println("end while loop"); System.err.flush(); + } catch (InterruptedException e) { + // SQUASH + } catch (IOException e) { + e.printStackTrace(); + done = true; + } + } // while ((done == false) && (stopReaderThread == false)) + // System.err.println("*** run() exiting..."); System.err.flush(); + } + + // ------------------------------------------------------------------------ + // ECMA48Terminal --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Getter for sessionInfo. + * + * @return the SessionInfo + */ + public SessionInfo getSessionInfo() { + return sessionInfo; + } + + /** + * Get the output writer. + * + * @return the Writer + */ + public PrintWriter getOutput() { + return output; + } + + /** + * Call 'stty' to set cooked mode. + * + *

Actually executes '/bin/sh -c stty sane cooked < /dev/tty' + */ + private void sttyCooked() { + doStty(false); + } + + /** + * Call 'stty' to set raw mode. + * + *

Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip + * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten + * -parenb cs8 min 1 < /dev/tty' + */ + private void sttyRaw() { + doStty(true); + } + + /** + * Call 'stty' to set raw or cooked mode. + * + * @param mode if true, set raw mode, otherwise set cooked mode + */ + private void doStty(final boolean mode) { + String [] cmdRaw = { + "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty" + }; + String [] cmdCooked = { + "/bin/sh", "-c", "stty sane cooked < /dev/tty" + }; + try { + Process process; + if (mode) { + process = Runtime.getRuntime().exec(cmdRaw); + } else { + process = Runtime.getRuntime().exec(cmdCooked); + } + BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); + String line = in.readLine(); + if ((line != null) && (line.length() > 0)) { + System.err.println("WEIRD?! Normal output from stty: " + line); + } + while (true) { + BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8")); + line = err.readLine(); + if ((line != null) && (line.length() > 0)) { + System.err.println("Error output from stty: " + line); + } + try { + process.waitFor(); + break; + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + int rc = process.exitValue(); + if (rc != 0) { + System.err.println("stty returned error code: " + rc); } + } catch (IOException e) { + e.printStackTrace(); } } @@ -604,6 +801,7 @@ public final class ECMA48Terminal extends LogicalScreen // Now emit only the modified attributes if ((lCell.getForeColor() != lastAttr.getForeColor()) && (lCell.getBackColor() != lastAttr.getBackColor()) + && (!lCell.isRGB()) && (lCell.isBold() == lastAttr.isBold()) && (lCell.isReverse() == lastAttr.isReverse()) && (lCell.isUnderline() == lastAttr.isUnderline()) @@ -616,8 +814,25 @@ public final class ECMA48Terminal extends LogicalScreen if (debugToStderr) { System.err.printf("1 Change only fore/back colors\n"); } + + } else if (lCell.isRGB() + && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB()) + && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB()) + && (lCell.isBold() == lastAttr.isBold()) + && (lCell.isReverse() == lastAttr.isReverse()) + && (lCell.isUnderline() == lastAttr.isUnderline()) + && (lCell.isBlink() == lastAttr.isBlink()) + ) { + // Both colors changed, attributes the same + sb.append(colorRGB(lCell.getForeColorRGB(), + lCell.getBackColorRGB())); + + if (debugToStderr) { + System.err.printf("1 Change only fore/back colors (RGB)\n"); + } } else if ((lCell.getForeColor() != lastAttr.getForeColor()) && (lCell.getBackColor() != lastAttr.getBackColor()) + && (!lCell.isRGB()) && (lCell.isBold() != lastAttr.isBold()) && (lCell.isReverse() != lastAttr.isReverse()) && (lCell.isUnderline() != lastAttr.isUnderline()) @@ -635,6 +850,7 @@ public final class ECMA48Terminal extends LogicalScreen } } else if ((lCell.getForeColor() != lastAttr.getForeColor()) && (lCell.getBackColor() == lastAttr.getBackColor()) + && (!lCell.isRGB()) && (lCell.isBold() == lastAttr.isBold()) && (lCell.isReverse() == lastAttr.isReverse()) && (lCell.isUnderline() == lastAttr.isUnderline()) @@ -648,8 +864,25 @@ public final class ECMA48Terminal extends LogicalScreen if (debugToStderr) { System.err.printf("3 Change foreColor\n"); } + } else if (lCell.isRGB() + && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB()) + && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB()) + && (lCell.getForeColorRGB() >= 0) + && (lCell.getBackColorRGB() >= 0) + && (lCell.isBold() == lastAttr.isBold()) + && (lCell.isReverse() == lastAttr.isReverse()) + && (lCell.isUnderline() == lastAttr.isUnderline()) + && (lCell.isBlink() == lastAttr.isBlink()) + ) { + // Attributes same, foreColor different + sb.append(colorRGB(lCell.getForeColorRGB(), true)); + + if (debugToStderr) { + System.err.printf("3 Change foreColor (RGB)\n"); + } } else if ((lCell.getForeColor() == lastAttr.getForeColor()) && (lCell.getBackColor() != lastAttr.getBackColor()) + && (!lCell.isRGB()) && (lCell.isBold() == lastAttr.isBold()) && (lCell.isReverse() == lastAttr.isReverse()) && (lCell.isUnderline() == lastAttr.isUnderline()) @@ -662,8 +895,24 @@ public final class ECMA48Terminal extends LogicalScreen if (debugToStderr) { System.err.printf("4 Change backColor\n"); } + } else if (lCell.isRGB() + && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB()) + && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB()) + && (lCell.isBold() == lastAttr.isBold()) + && (lCell.isReverse() == lastAttr.isReverse()) + && (lCell.isUnderline() == lastAttr.isUnderline()) + && (lCell.isBlink() == lastAttr.isBlink()) + ) { + // Attributes same, foreColor different + sb.append(colorRGB(lCell.getBackColorRGB(), false)); + + if (debugToStderr) { + System.err.printf("4 Change backColor (RGB)\n"); + } } else if ((lCell.getForeColor() == lastAttr.getForeColor()) && (lCell.getBackColor() == lastAttr.getBackColor()) + && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB()) + && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB()) && (lCell.isBold() == lastAttr.isBold()) && (lCell.isReverse() == lastAttr.isReverse()) && (lCell.isUnderline() == lastAttr.isUnderline()) @@ -678,16 +927,29 @@ public final class ECMA48Terminal extends LogicalScreen } } else { // Just reset everything again - sb.append(color(lCell.getForeColor(), - lCell.getBackColor(), - lCell.isBold(), - lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - - if (debugToStderr) { - System.err.printf("6 Change all attributes\n"); + if (!lCell.isRGB()) { + sb.append(color(lCell.getForeColor(), + lCell.getBackColor(), + lCell.isBold(), + lCell.isReverse(), + lCell.isBlink(), + lCell.isUnderline())); + + if (debugToStderr) { + System.err.printf("6 Change all attributes\n"); + } + } else { + sb.append(colorRGB(lCell.getForeColorRGB(), + lCell.getBackColorRGB(), + lCell.isBold(), + lCell.isReverse(), + lCell.isBlink(), + lCell.isUnderline())); + if (debugToStderr) { + System.err.printf("6 Change all attributes (RGB)\n"); + } } + } // Emit the character sb.append(lCell.getChar()); @@ -712,11 +974,6 @@ public final class ECMA48Terminal extends LogicalScreen * physical screen */ private String flushString() { - if (!dirty) { - assert (!reallyCleared); - return ""; - } - CellAttributes attr = null; StringBuilder sb = new StringBuilder(); @@ -729,7 +986,6 @@ public final class ECMA48Terminal extends LogicalScreen flushLine(y, sb, attr); } - dirty = false; reallyCleared = false; String result = sb.toString(); @@ -739,37 +995,6 @@ public final class ECMA48Terminal extends LogicalScreen return result; } - /** - * Push the logical screen to the physical device. - */ - @Override - public void flushPhysical() { - String result = flushString(); - if ((cursorVisible) - && (cursorY >= 0) - && (cursorX >= 0) - && (cursorY <= height - 1) - && (cursorX <= width - 1) - ) { - result += cursor(true); - result += gotoXY(cursorX, cursorY); - } else { - result += cursor(false); - } - output.write(result); - flush(); - } - - /** - * Set the window title. - * - * @param title the new title - */ - public void setTitle(final String title) { - output.write(getSetTitleString(title)); - flush(); - } - /** * Reset keyboard/mouse input parser. */ @@ -1084,51 +1309,44 @@ public final class ECMA48Terminal extends LogicalScreen eventMouseWheelUp, eventMouseWheelDown); } - /** - * Return any events in the IO queue. - * - * @param queue list to append new events to - */ - public void getEvents(final List queue) { - synchronized (eventQueue) { - if (eventQueue.size() > 0) { - synchronized (queue) { - queue.addAll(eventQueue); - } - eventQueue.clear(); - } - } - } - /** * Return any events in the IO queue due to timeout. * * @param queue list to append new events to */ private void getIdleEvents(final List queue) { - Date now = new Date(); + long nowTime = System.currentTimeMillis(); // Check for new window size - long windowSizeDelay = now.getTime() - windowSizeTime; + long windowSizeDelay = nowTime - windowSizeTime; if (windowSizeDelay > 1000) { sessionInfo.queryWindowSize(); int newWidth = sessionInfo.getWindowWidth(); int newHeight = sessionInfo.getWindowHeight(); + if ((newWidth != windowResize.getWidth()) || (newHeight != windowResize.getHeight()) ) { + + if (debugToStderr) { + System.err.println("Screen size changed, old size " + + windowResize); + System.err.println(" new size " + + newWidth + " x " + newHeight); + } + TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN, newWidth, newHeight); windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, newWidth, newHeight); queue.add(event); } - windowSizeTime = now.getTime(); + windowSizeTime = nowTime; } // ESCDELAY type timeout if (state == ParseState.ESCAPE) { - long escDelay = now.getTime() - escapeTime; + long escDelay = nowTime - escapeTime; if (escDelay > 100) { // After 0.1 seconds, assume a true escape character queue.add(controlChar((char)0x1B, false)); @@ -1192,9 +1410,9 @@ public final class ECMA48Terminal extends LogicalScreen private void processChar(final List events, final char ch) { // ESCDELAY type timeout - Date now = new Date(); + long nowTime = System.currentTimeMillis(); if (state == ParseState.ESCAPE) { - long escDelay = now.getTime() - escapeTime; + long escDelay = nowTime - escapeTime; if (escDelay > 250) { // After 0.25 seconds, assume a true escape character events.add(controlChar((char)0x1B, false)); @@ -1214,7 +1432,7 @@ public final class ECMA48Terminal extends LogicalScreen if (ch == 0x1B) { state = ParseState.ESCAPE; - escapeTime = now.getTime(); + escapeTime = nowTime; return; } @@ -1545,6 +1763,55 @@ public final class ECMA48Terminal extends LogicalScreen rgbColor(bold, color, foreground); } + /** + * Create a T.416 RGB parameter sequence for a single color change. + * + * @param colorRGB a 24-bit RGB value for foreground color + * @param foreground if true, this is a foreground color + * @return the string to emit to an ANSI / ECMA-style terminal, + * e.g. "\033[42m" + */ + private String colorRGB(final int colorRGB, final boolean foreground) { + + int colorRed = (colorRGB >> 16) & 0xFF; + int colorGreen = (colorRGB >> 8) & 0xFF; + int colorBlue = colorRGB & 0xFF; + + StringBuilder sb = new StringBuilder(); + if (foreground) { + sb.append("\033[38;2;"); + } else { + sb.append("\033[48;2;"); + } + sb.append(String.format("%d;%d;%dm", colorRed, colorGreen, colorBlue)); + return sb.toString(); + } + + /** + * Create a T.416 RGB parameter sequence for both foreground and + * background color change. + * + * @param foreColorRGB a 24-bit RGB value for foreground color + * @param backColorRGB a 24-bit RGB value for foreground color + * @return the string to emit to an ANSI / ECMA-style terminal, + * e.g. "\033[42m" + */ + private String colorRGB(final int foreColorRGB, final int backColorRGB) { + int foreColorRed = (foreColorRGB >> 16) & 0xFF; + int foreColorGreen = (foreColorRGB >> 8) & 0xFF; + int foreColorBlue = foreColorRGB & 0xFF; + int backColorRed = (backColorRGB >> 16) & 0xFF; + int backColorGreen = (backColorRGB >> 8) & 0xFF; + int backColorBlue = backColorRGB & 0xFF; + + StringBuilder sb = new StringBuilder(); + sb.append(String.format("\033[38;2;%d;%d;%dm", + foreColorRed, foreColorGreen, foreColorBlue)); + sb.append(String.format("\033[48;2;%d;%d;%dm", + backColorRed, backColorGreen, backColorBlue)); + return sb.toString(); + } + /** * Create a T.416 RGB parameter sequence for a single color change. * @@ -1766,6 +2033,77 @@ public final class ECMA48Terminal extends LogicalScreen return sb.toString(); } + /** + * Create a SGR parameter sequence for foreground, background, and + * several attributes. This sequence first resets all attributes to + * default, then sets attributes as per the parameters. + * + * @param foreColorRGB a 24-bit RGB value for foreground color + * @param backColorRGB a 24-bit RGB value for foreground color + * @param bold if true, set bold + * @param reverse if true, set reverse + * @param blink if true, set blink + * @param underline if true, set underline + * @return the string to emit to an ANSI / ECMA-style terminal, + * e.g. "\033[0;1;31;42m" + */ + private String colorRGB(final int foreColorRGB, final int backColorRGB, + final boolean bold, final boolean reverse, final boolean blink, + final boolean underline) { + + int foreColorRed = (foreColorRGB >> 16) & 0xFF; + int foreColorGreen = (foreColorRGB >> 8) & 0xFF; + int foreColorBlue = foreColorRGB & 0xFF; + int backColorRed = (backColorRGB >> 16) & 0xFF; + int backColorGreen = (backColorRGB >> 8) & 0xFF; + int backColorBlue = backColorRGB & 0xFF; + + StringBuilder sb = new StringBuilder(); + if ( bold && reverse && blink && !underline ) { + sb.append("\033[0;1;7;5;"); + } else if ( bold && reverse && !blink && !underline ) { + sb.append("\033[0;1;7;"); + } else if ( !bold && reverse && blink && !underline ) { + sb.append("\033[0;7;5;"); + } else if ( bold && !reverse && blink && !underline ) { + sb.append("\033[0;1;5;"); + } else if ( bold && !reverse && !blink && !underline ) { + sb.append("\033[0;1;"); + } else if ( !bold && reverse && !blink && !underline ) { + sb.append("\033[0;7;"); + } else if ( !bold && !reverse && blink && !underline) { + sb.append("\033[0;5;"); + } else if ( bold && reverse && blink && underline ) { + sb.append("\033[0;1;7;5;4;"); + } else if ( bold && reverse && !blink && underline ) { + sb.append("\033[0;1;7;4;"); + } else if ( !bold && reverse && blink && underline ) { + sb.append("\033[0;7;5;4;"); + } else if ( bold && !reverse && blink && underline ) { + sb.append("\033[0;1;5;4;"); + } else if ( bold && !reverse && !blink && underline ) { + sb.append("\033[0;1;4;"); + } else if ( !bold && reverse && !blink && underline ) { + sb.append("\033[0;7;4;"); + } else if ( !bold && !reverse && blink && underline) { + sb.append("\033[0;5;4;"); + } else if ( !bold && !reverse && !blink && underline) { + sb.append("\033[0;4;"); + } else { + assert (!bold && !reverse && !blink && !underline); + sb.append("\033[0;"); + } + + sb.append("m\033[38;2;"); + sb.append(String.format("%d;%d;%d", foreColorRed, foreColorGreen, + foreColorBlue)); + sb.append("m\033[48;2;"); + sb.append(String.format("%d;%d;%d", backColorRed, backColorGreen, + backColorBlue)); + sb.append("m"); + return sb.toString(); + } + /** * Create a SGR parameter sequence to reset to defaults. * @@ -1862,78 +2200,4 @@ public final class ECMA48Terminal extends LogicalScreen return "\033[?1002;1003;1006;1005l\033[?1049l"; } - /** - * Read function runs on a separate thread. - */ - public void run() { - boolean done = false; - // available() will often return > 1, so we need to read in chunks to - // stay caught up. - char [] readBuffer = new char[128]; - List events = new LinkedList(); - - while (!done && !stopReaderThread) { - try { - // We assume that if inputStream has bytes available, then - // input won't block on read(). - int n = inputStream.available(); - if (n > 0) { - if (readBuffer.length < n) { - // The buffer wasn't big enough, make it huger - readBuffer = new char[readBuffer.length * 2]; - } - - int rc = input.read(readBuffer, 0, readBuffer.length); - // System.err.printf("read() %d", rc); System.err.flush(); - if (rc == -1) { - // This is EOF - done = true; - } else { - for (int i = 0; i < rc; i++) { - int ch = readBuffer[i]; - processChar(events, (char)ch); - } - getIdleEvents(events); - if (events.size() > 0) { - // Add to the queue for the backend thread to - // be able to obtain. - synchronized (eventQueue) { - eventQueue.addAll(events); - } - if (listener != null) { - synchronized (listener) { - listener.notifyAll(); - } - } - events.clear(); - } - } - } else { - getIdleEvents(events); - if (events.size() > 0) { - synchronized (eventQueue) { - eventQueue.addAll(events); - } - events.clear(); - if (listener != null) { - synchronized (listener) { - listener.notifyAll(); - } - } - } - - // Wait 10 millis for more data - Thread.sleep(10); - } - // System.err.println("end while loop"); System.err.flush(); - } catch (InterruptedException e) { - // SQUASH - } catch (IOException e) { - e.printStackTrace(); - done = true; - } - } // while ((done == false) && (stopReaderThread == false)) - // System.err.println("*** run() exiting..."); System.err.flush(); - } - }