org.kohsuke.ajaxterm.Terminal Maven / Gradle / Ivy
The newest version!
package org.kohsuke.ajaxterm;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Math.max;
import static java.lang.Math.min;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
/**
* Screen buffer.
*
* @author Kohsuke Kawaguchi
*/
public class Terminal {
/**
* This is typed as 'char' but it's not character that's stored.
* The lower 8 bit is the character code, and upper 8 bit are back/fore color.
*
* 0xFBCC
* ^^~~ <- ASCII char code
* ||
* |+-- background color code (0:black, 1:blue, 2:red, 4:green, ....)
* |
* +--- foreground color code
*/
public char[] scr;
/**
* Screen width and height.
*/
public final int width,height;
/**
* Scroll region.
*/
private int st,sb;
/**
* Current cursor position.
*/
private int cx,cy;
/**
* Cursor back up position.
*/
private int cx_bak,cy_bak;
private boolean cl;
/**
* Set graphics rendition. This is the value that gets stored into the higher 8 bits of {@link #scr}
*
* @see #csi_m(int[])
*/
private int sgr;
private String buf; // TODO: switch to StringBuilder
private String outbuf;
/**
* The HTML that we returned from {@link #dumpHtml(boolean,int)} the last time.
*/
private String last_html;
/**
* The value of {@link #timestamp} when we computed {@link #last_html}
*/
private int last_html_timestamp;
/**
* Unique counter that increases as the screen changes.
*
* Don't start by 0 as that's the typical client's initial value.
*/
private int timestamp = 1000;
/**
* True if the cursor should be displayed.
*/
public boolean showCursor;
private String cssClass;
public Terminal(int width, int height) {
this.width = width;
this.height = height;
reset();
}
/**
* Sets additional CSS classes for the terminal element.
*/
public void setCssClass(String cssClass) {
this.cssClass = cssClass;
}
public int getCx() {
return cx;
}
public int getCy() {
return cy;
}
@Esc("\u001Bc")
public void reset() {
scr = new char[width*height];
Arrays.fill(scr,EMPTY_CH);
st = 0;
sb = height-1;
cx_bak = cx = 0;
cy_bak = cy = 0;
cl = false;
sgr = 0x700;
showCursor = true;
buf = outbuf = last_html = "";
timestamp += 1000;
}
private int $(int y, int x) {
return y*width+x;
}
public String peek(int y1, int y2) {
return peek(y1,0,y2,width);
}
public String peek(int y1, int x1, int y2, int x2) {
int s = $(y1,x1);
return new String(scr,s,$(y2,x2)-s);
}
public void poke(int y, int x, String s) {
// TODO: i18n
char[] chars = s.toCharArray();
int destPos = $(y, x);
System.arraycopy(chars,0,scr, destPos, min(chars.length, scr.length - destPos));
}
public void poke(int y, String s) {
poke(y,0,s);
}
public void zero(int y1, int x1, int y2, int x2) {
int e = $(y2,x2);
for( int i= $(y1,x1); i=st && cy<=sb) {
cl=false;
int q = (cy+1)/(sb+1);
if(q!=0) {
scrollUp(st,sb);
cy = sb;
} else {
cy = (cy+1)%(sb+1);
}
}
}
public void cursorRight() {
if((cx+1)>=width)
cl=true;
else
cx = (cx+1)%width;
}
public void echo(char c) {
if(cl) {
cursorDown();
cx = 0;
}
scr[$(cy,cx)] = (char)(sgr|c);
cursorRight();
}
public void escape() {
if(buf.length()>32) {
// error
if(LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Unhandled escape sequence: "+buf.replaceAll("\u001B",""));
buf = "";
return;
}
EscapeSequence es = ESCAPE_SEQUENCES.get(buf);
if(es!=null) {
es.handle(this, buf,null);
buf = "";
return;
}
for (Entry ent : REGEXP_ESCAPE_SEQUENCES.entrySet()) {
Matcher m = ent.getKey().matcher(buf);
if(m.matches()) {
ent.getValue().handle(this, buf,m);
buf = "";
return;
}
}
}
/**
* Receives the output from the forked process into the terminal.
*/
public void write(String s) {
timestamp++;
if (LOGGER.isLoggable(Level.FINEST))
LOGGER.finest("Received: "+s);
for( int i=0; i0 || ESCAPE_SEQUENCES.containsKey(""+ch)) {
buf += ch;
escape();
} else
if(ch=='\u001B') {
buf += ch;
} else {
echo(ch);
}
}
}
public String read() {
String b = outbuf;
outbuf = null;
return b;
}
public String dump() {
StringBuilder buf = new StringBuilder(scr.length);
for (char ch : scr)
buf.append((char) (ch&0xFF));
return buf.toString();
}
public String dumpLatin1() {
StringBuilder buf = new StringBuilder(scr.length);
int i=0;
for (char ch : scr) {
buf.append(LATEN1_TABLE.charAt((ch&0xFF)));
if (++i%width==0)
buf.append('\n');
}
return buf.toString();
}
private int pack(int fg, int bg, boolean cursor) {
return (cursor?1<<8:0)+(fg<<4)+(bg);
}
/**
* @param color
* If we want the color coded output. It'll make the response bit bigger.
* @param clientTimestamp
* The value of {@link ScreenImage#timestamp} that the client currently has.
* This information is used to avoid unnecessary screen refresh.
*/
public ScreenImage dumpHtml(boolean color, int clientTimestamp) {
if (timestamp==clientTimestamp) // our screen hasn't changed
return new ScreenImage(clientTimestamp, NO_CHANGE, this);
StringBuilder r = new StringBuilder(cx*cy*2);
r.append("");
int currentStatus = -1;
int total = height * width;
for( int i=0; i< total; i++) {
int q = scr[i]/256;
int bg,fg;
if(color) {
bg = q/16;
fg = q%16;
} else {
bg = 1;
fg = 7;
}
boolean cursor = $(cy,cx)==i;
int p = pack(fg,bg,cursor);
if(currentStatus!=p) {// rendering status has changed
currentStatus = p;
if(i!=0) r.append("");
r.append("");
}
int c = scr[i]%256;
switch (c) {
case '<': r.append("<");break;
case '&': r.append("&");break;
default: r.append(HTML_TABLE.charAt(c));break;
}
if((i+1)%width ==0) r.append('\n');
}
r.append("
");
String str = r.toString();
if(str.equals(last_html) && last_html_timestamp==clientTimestamp) {
return new ScreenImage(clientTimestamp,NO_CHANGE,this);
} else {
last_html = str;
last_html_timestamp = timestamp;
return new ScreenImage(timestamp,str,this);
}
}
@Esc({"\u0005","\u001B[c","\u001B[0c","\u001BZ"})
public void esc_da() {
outbuf = "\u001B[?6c";
}
/**
* Backspace.
*/
@Esc("\u0008")
public void esc_0x08() {
cx = max(0,cx-1);
}
/**
* Tab.
*/
@Esc("\u0009")
public void esc_0x09() {
cx = (((cx/8)+1)*8)%width;
}
/**
* Carriage return
*/
@Esc("\r")
public void esc_0x0d() {
cl=false;
cx=0;
}
@Esc({"\u0000","\u0007","\u000E","\u000F","\u001B#8",
"\u001B=","\u001B>","\u001B(0","\u001B(A",
"\u001B(B","\u001B]R","\u001BD","\u001BE","\u001BH",
"\u001BN","\u001BO","\u001Ba","\u001Bn","\u001Bo"})
public void noOp() {
}
@Esc("\u001B7")
public void saveCursor() {
cx_bak = cx;
cy_bak = cy;
}
@Esc("\u001B8")
public void restoreCursor() {
cx = cx_bak;
cy = cy_bak;
}
@Esc("\u001BM")
public void escRi() {
cy = max(st,cy-1);
if(cy==st)
scrollDown(st,sb);
}
public void csi_A(int[] i) {
cy = max(st,cy-defaultsTo(i,1));
}
public void csi_B(int[] i) {
cy = min(sb,cy+defaultsTo(i,1));
}
public void csi_C(int[] i) {
cx = min(width-1,cx+defaultsTo(i,1));
cl = false;
}
public void csi_D(int[] i) {
cx = max(0,cx-defaultsTo(i,1));
cl = false;
}
/**
* args[0], otherwise defaults to 'defaultValue'
*/
private int defaultsTo(int[] args, int defaultValue) {
return (args.length==0) ? defaultValue : args[0];
}
public void csi_E(int[] i) {
csi_B(i);
cx = 0;
cl = false;
}
public void csi_F(int[] i) {
csi_A(i);
cx = 0;
cl = false;
}
public void csi_G(int[] i) {
cx = min(width,i[0])-1;
}
public void csi_H(int[] i) {
if(i.length<2) i=new int[]{1,1};
cx = min(width,i[1])-1;
cy = min(height,i[0])-1;
cl = false;
}
public void csi_J(int[] i) {
switch (defaultsTo(i,0)) {
case 0: zero(cy,cx,height,0);return;
case 1: zero(0,0,cx,cy);return;
case 2: zero(0,0,height,0);return;
}
}
public void csi_K(int... i) {
switch (defaultsTo(i,0)) {
case 0: zero(cy,cx,cy,width);return;
case 1: zero(cy,0,cy,cx);return;
case 2: zero(cy,0,cy,width);return;
}
}
/**
* Insert lines.
*/
public void csi_L(int[] args) {
for(int i=0;i=st && cy<=sb)
for(int i=0;i ESCAPE_SEQUENCES = new HashMap();
private static final Map REGEXP_ESCAPE_SEQUENCES = new HashMap();
private static abstract class CsiSequence {
final int arg2; // ?
protected CsiSequence(int arg2) {
this.arg2 = arg2;
}
abstract void handle(Terminal t, int[] args);
}
private static final Map CSI_SEQUENCE = new HashMap();
private static final String HTML_TABLE, LATEN1_TABLE;
static {
for( final Method m : Terminal.class.getMethods() ) {
Esc esc = m.getAnnotation(Esc.class);
if(esc!=null) {
for( String s : esc.value() ) {
ESCAPE_SEQUENCES.put(s,new EscapeSequence() {
public void handle(Terminal t, String s, Matcher _) {
try {
m.invoke(t);
} catch (IllegalAccessException e) {
throw new IllegalAccessError();
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
});
}
}
if(m.getName().startsWith("csi_") && m.getName().length()==5) {
CSI_SEQUENCE.put(m.getName().charAt(4),new CsiSequence(1) {
void handle(Terminal t, int[] args) {
try {
m.invoke(t,new Object[]{args});
} catch (IllegalAccessException e) {
throw new IllegalAccessError();
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
});
}
}
REGEXP_ESCAPE_SEQUENCES.put(
Pattern.compile("\u001B\\[\\??([0-9;]*)([@ABCDEFGHJKLMPXacdefghlmnqrstu`])"),
new EscapeSequence() {
public void handle(Terminal t, String _, Matcher m) {
String s = m.group(1);
CsiSequence seq = CSI_SEQUENCE.get(m.group(2).charAt(0));
if(seq!=null) {
String[] tokens = s.split(";");
if (s.length()==0)
tokens = EMPTY_STRING_ARRAY;
int[] n = new int[tokens.length];
for (int i = 0; i < n.length; i++)
try {
n[i] = Integer.parseInt(tokens[i]);
} catch (NumberFormatException e) {
n[i] = 0;
}
seq.handle(t,n);
}
}
});
REGEXP_ESCAPE_SEQUENCES.put(
Pattern.compile("\u001C([^\u0007]+)\u0007"),
NONE);
CSI_SEQUENCE.put('@',new CsiSequence(1) {
@Override
void handle(Terminal t, int[] args) {
for( int i=0; i
© 2015 - 2025 Weber Informatics LLC | Privacy Policy