/* $Log: PScreen.java,v $ * Revision 1.3 2000/03/07 03:52:16 stuart * profiling revealed charToByte as a bottleneck. Relieved by * creating another OutputStreamWriter for its use. * * Revision 1.2 2000/03/07 02:23:18 stuart * initial double buffering support * */ package bmsi.fsp; import java.io.*; import java.net.Socket; /** A physical TUI screen on an ASCII terminal. Communication with each screen is carried out by means of an InputStream and an OutputStream. A terminal type is used to lookup a TermInfo object to provide the byte strings which control various terminal functions and are sent by function keys on the terminal. @author Stuart D. Gathman Copyright (C) 2000 Business Management Systems, Inc. */ public class PScreen extends Writer { private TermInfo ti = new TermInfo(); private OutputStream out; private int rows, cols; private int prow, pcol; private char[] curscr; private char[] stdscr; private static class rowdata { int data; // offset in stdscr of this row int srow; // offset in curscr of original screen row int modc; // first modified column } private rowdata[] lines; /* Keep references to often used sequences handy. */ private byte[] cursor_home; private byte[] cursor_address; private byte[] cursor_right; private byte[] cursor_left; private byte[] cursor_up; private byte[] cursor_down; private byte[] carriage_return; private byte[] column_address; private byte[] parm_left_cursor; private byte[] parm_right_cursor; private byte[] parm_up_cursor; private byte[] parm_down_cursor; private byte[] clr_eol; private byte[] repeat_char; private boolean auto_margin; /** Internally, the screen is maintained as 7 bit ascii plus 9 bits of attributes. The ALTCHARSET attribute can be used to form an 8 bit encoding - typically Cp0437. An additional attribute (typically bold) can be used to select another 8 bit encoding for a total of 512 font positions in the typical ascii terminal.

Applications will use Unicode to specify line drawing, etc. But some terminals have non standard encodings not supported by Java directly. We use sun.io.CharToByteConverter to supply custom encodings for such terminals. We can create a mediocre custom encoding from the terminfo using ascmap (which specifies an encoding from the vt100 encoding) and box (which specifies 11 or 22 line drawing chars), but high quality support will require a custom class. */ private int row,col; // cursor position private int attr; // text attribute private OutputStream scrnout = new ScreenOutput(); private OutputStreamWriter wtr; private OutputStreamWriter cvtr; private Keyboard keyb; private byte cvtb; public PScreen(String term,Socket sock) throws IOException { out = new BufferedOutputStream(sock.getOutputStream()); keyb = new Keyboard(sock.getInputStream()); setTerm(term); wtr = new OutputStreamWriter(scrnout,"Cp437"); cvtr = new OutputStreamWriter(new OutputStream() { public void write(int b) { cvtb = (byte)b; } },"Cp437"); } public void write(char[] buf,int pos,int len) throws IOException { wtr.write(buf,pos,len); } public void flush() throws IOException { sync(); } public void setTerm(String term) throws IOException { ti.setName(term); cols = ti.getNum(TI.COLUMNS); rows = ti.getNum(TI.LINES); cursor_home = ti.getStr(TI.CURSOR_HOME); cursor_address = ti.getStr(TI.CURSOR_ADDRESS); cursor_left = ti.getStr(TI.CURSOR_LEFT); cursor_right = ti.getStr(TI.CURSOR_RIGHT); cursor_up = ti.getStr(TI.CURSOR_UP); cursor_down = ti.getStr(TI.CURSOR_DOWN); column_address = ti.getStr(TI.COLUMN_ADDRESS); carriage_return = ti.getStr(TI.CARRIAGE_RETURN); clr_eol = ti.getStr(TI.CLR_EOL); repeat_char = ti.getStr(TI.REPEAT_CHAR); parm_left_cursor = ti.getStr(TI.PARM_LEFT_CURSOR); parm_right_cursor = ti.getStr(TI.PARM_RIGHT_CURSOR); parm_up_cursor = ti.getStr(TI.PARM_UP_CURSOR); parm_down_cursor = ti.getStr(TI.PARM_DOWN_CURSOR); auto_margin = ti.getBool(TI.AUTO_RIGHT_MARGIN); keyb.clear(); int[] a = ti.getKeys(); for (int i = 0; i < a.length; ++i) { byte[] b = ti.getStr(a[i]); if (b != null) keyb.defineKey(b,a[i] + 256); } // default KEY_OPTIONS to ^O if (ti.getStr(TI.KEY_OPTIONS) == null) keyb.defineKey(new byte[] { (byte)017 },TI.KEY_OPTIONS + 256); // default KEY_EXIT to ^C if (ti.getStr(TI.KEY_EXIT) == null) keyb.defineKey(new byte[] { (byte)003 },TI.KEY_EXIT + 256); // initialize screen buffers int n = rows * cols; curscr = new char[n]; fill(curscr,0,n,' ' + TI.A_NORMAL); stdscr = new char[n]; fill(stdscr,0,n,' ' + TI.A_NORMAL); lines = new rowdata[rows]; for (int i = 0; i < lines.length; ++i) { rowdata r = new rowdata(); r.modc = cols; r.data = i * cols; r.srow = i * cols; lines[i] = r; } putp(TI.ENTER_CA_MODE); putp(TI.EXIT_ATTRIBUTE_MODE); vidinit(TI.A_NORMAL); putp(TI.CLEAR_SCREEN); } public static void fill(char[] b,int pos,int len,int val) { if (len <= 0) return; b[pos] = (char)val; int i = 1; for (; i + i < len; i += i) System.arraycopy( b, pos, b, pos + i, i); System.arraycopy( b, pos, b, pos + i, len - i); } public int getRows() { return rows; } public int getCols() { return cols; } /** Convert just one char to a byte. This is convenient, but too inefficient when converting more than one char. */ private synchronized byte charToByte(char c) throws IOException { cvtr.write(c); cvtr.flush(); return cvtb; } public void fill(int x,int y,int w,int h,char c) throws IOException { int ye = y + h; for (int r = y; r < ye; ++r) { rowdata p = lines[r]; fill(stdscr,p.data + x,w,(charToByte(c) & 0xFF) | attr); if (x < p.modc) p.modc = x; } } /** Copy an area of the screen to another position. Any insert/delete capabilities of the terminal are used to make this as efficient as possible. However, at a minimum the area left behind will need to be repainted. If the terminal has no insert/delete functions at all, the entire target area will need to be repainted as well. Most terminals will need some repainting because, for instance, they can insert/delete full lines, but not partial lines. */ public void copyArea(int x,int y,int w,int h,int dx,int dy) { // punt, and just repaint the rectangle containing the area // and its copy. if (dx < 0) { x += dx; w -= dx; } else { w += dx; } if (dy < 0) { y += dy; w -= dy; } else { h += dy; } repaint(x,y,w,h); } public void repaint(int x,int y,int w,int h) { } public void sync() throws IOException { wtr.flush(); for (int r = 0; r < rows; ++r) { rowdata p = lines[r]; int c = p.modc; int pos = p.data + c; int cpos = r * cols + c; int i = c; while (c < cols) { while (i < cols && stdscr[pos] != curscr[cpos]) { ++pos; ++cpos; ++i; } if (i > c) { move(prow,pcol,r,c); prow = r; pcol = c; int n = i - c; pos -= n; cpos -= n; while (n-- > 0) { char ch = stdscr[pos++]; vidattr(ch & TI.A_ATTRIBUTES); //twrite(ch & TI.A_CHARTEXT); twrite(ch & 0xFF); curscr[cpos++] = ch; } } while (i < cols && stdscr[pos] == curscr[cpos]) { ++pos; ++cpos; ++i; } c = i; } } move(prow,pcol,row,col); prow = row; pcol = col; out.flush(); } /** Write 8-bit bytes to screen. There is ridiculous overhead looking up an encoding in JDK, so translating individual Strings with getBytes() is too slow. Using sun.io.CharToByteConverter directly is not 100% pure. So we use an OutputStreamWriter to translate for us, and pass it this special output stream that writes to the screen while tracking the cursor position. */ class ScreenOutput extends OutputStream { /** Copy bytes to the screen while tracking the cursor location. */ public void write(int c) { int pos = lines[row].data + col; stdscr[pos] = (char)((c & TI.A_CHARTEXT) | attr); if (++col >= cols) { if (++row >= rows) { --row; --col; return; // don't write in bottom right corner } col = 0; } } } /** Copy bytes to the screen while tracking the cursor location. */ private void twrite(int c) throws IOException { if (++pcol >= cols) { if (auto_margin) { if (++prow >= rows) { --prow; --pcol; return; // don't write in bottom right corner } pcol = 0; } else --pcol; } if (c < ' ') { byte[] b = new byte[] { 27, (byte)'U', 0, 27, (byte)'u' }; b[2] = (byte)c; out.write(b); // trace.write(b); } else out.write(c); // trace.write(c); } public void setBufPos(int x,int y) throws IOException { wtr.flush(); row = y; col = x; } private char magic_modes; private char magic_exits; private int sgr_mode; private byte[] set_attributes; /** Return bitmask of attributes that are "cookies". @param ch initial attributes of terminal */ private char vidinit(char ch) throws IOException { int magic_cookies = 0; magic_modes = 0; if (ti.getStr(TI.ENTER_ALT_CHARSET_MODE) != null) magic_modes |= TI.A_ALTCHARSET; if (ti.getStr(TI.ENTER_BLINK_MODE) != null) magic_modes |= TI.A_BLINK; if (ti.getStr(TI.ENTER_BOLD_MODE) != null) magic_modes |= TI.A_BOLD; if (ti.getStr(TI.ENTER_DIM_MODE) != null) magic_modes |= TI.A_DIM; if (ti.getStr(TI.ENTER_PROTECTED_MODE) != null) magic_modes |= TI.A_PROTECT; if (ti.getStr(TI.ENTER_REVERSE_MODE) != null) magic_modes |= TI.A_REVERSE; if (ti.getStr(TI.ENTER_SECURE_MODE) != null) magic_modes |= TI.A_INVIS; if (ti.getStr(TI.ENTER_STANDOUT_MODE) != null) magic_modes |= TI.A_STANDOUT; if (ti.getStr(TI.ENTER_UNDERLINE_MODE) != null) magic_modes |= TI.A_UNDERLINE; magic_exits = 0; if (ti.getStr(TI.EXIT_ALT_CHARSET_MODE) != null) magic_exits |= TI.A_ALTCHARSET; if (ti.getStr(TI.EXIT_UNDERLINE_MODE) != null) magic_exits |= TI.A_UNDERLINE; if (ti.getStr(TI.EXIT_STANDOUT_MODE) != null) magic_exits |= TI.A_STANDOUT; set_attributes = ti.getStr(TI.SET_ATTRIBUTES); int magic_cookie_glitch = ti.getNum(TI.MAGIC_COOKIE_GLITCH); if (magic_cookie_glitch > 0) throw new IOException("Space cookie attributes not supported"); if (magic_cookie_glitch == 0) magic_cookies = ~magic_modes & TI.A_ATTRIBUTES & ~TI.A_MODIFIED; sgr_mode = ch; return (char)magic_cookies; } private int doattr(int cap,int ch) throws IOException { if (ch != 0) { byte[] str = ti.getStr(cap); if (str != null) { putp(str); return ~ch; } } return ~0; } /** Set text attributes. There is no indication in terminfo of which attributes are cumulative. We assume that attributes defined with "enter_*_mode" and "exit_*_mode" are cumulative, whereas sgr is not. In addition, cookies are never cumulative. Terminfo doesn't tell us which attributes are cookies, but we will assume that if magic_cookie_glitch >= 0, then all attributes set with sgr are cookies. We make a bit mask of cookie attributes available to the caller. sgr is assumed to set any attributes not set with an enter_*_mode command. I.e., we try to use enter_*_mode first.

sgr0 must be defined to turn off any enter_*_mode attributes that have no corresponding exit_*_mode command. sgr0 should turn off *all* such cumulative attributes, but should not affect sgr.

Like AT&T, we keep track of the current mode attributes in CUR sgr_mode and only output the changes in vidattr(). Unlike AT&T, we can output cookies by setting A_MODIFIED. FIXME: no cookie support yet. */ private void vidattr(int ch) throws IOException { if (sgr_mode == ch) return; //System.err.println("sgr "+Integer.toHexString(sgr_mode) // +" -> "+Integer.toHexString(ch)); int on = ch & ~sgr_mode; int off = sgr_mode & ~ch; sgr_mode = ch & ~TI.A_MODIFIED; if (off != 0) { /* some need to be turned off */ if ((off & ~magic_exits & magic_modes) != 0) { off &= doattr(TI.EXIT_ATTRIBUTE_MODE,off & magic_modes); on |= ch & magic_modes; } off &= doattr(TI.EXIT_ALT_CHARSET_MODE,off & TI.A_ALTCHARSET); off &= doattr(TI.EXIT_UNDERLINE_MODE,off & TI.A_UNDERLINE); off &= doattr(TI.EXIT_STANDOUT_MODE,off & TI.A_STANDOUT); } if (on != 0) { /* some need to be turned on */ on &= doattr(TI.ENTER_ALT_CHARSET_MODE,on & TI.A_ALTCHARSET); on &= doattr(TI.ENTER_STANDOUT_MODE,on & TI.A_STANDOUT); on &= doattr(TI.ENTER_BOLD_MODE,on & TI.A_BOLD); on &= doattr(TI.ENTER_REVERSE_MODE,on & TI.A_REVERSE); on &= doattr(TI.ENTER_BLINK_MODE,on & TI.A_BLINK); on &= doattr(TI.ENTER_UNDERLINE_MODE,on & TI.A_UNDERLINE); on &= doattr(TI.ENTER_PROTECTED_MODE,on & TI.A_PROTECT); on &= doattr(TI.ENTER_SECURE_MODE,on & TI.A_INVIS); on &= doattr(TI.ENTER_DIM_MODE,on & TI.A_DIM); } if (on == 0 && off == 0) return; // all changes handled via modes /* still some to turn on/off or a cookie to deposit */ if (set_attributes != null) putp(ti.tparm(set_attributes, new int[] { (ch&TI.A_STANDOUT), (ch&TI.A_UNDERLINE), (ch&TI.A_REVERSE), (ch&TI.A_BLINK), (ch&TI.A_DIM), (ch&TI.A_BOLD), (ch&TI.A_INVIS), (ch&TI.A_PROTECT), (ch&TI.A_ALTCHARSET) })); } public void setAttr(char ch) { attr = ch & TI.A_ATTRIBUTES; } public void write(int x,int y,int w,String s,char attr) throws IOException { if (x < 0) { s = s.substring(-x); w += x; x = 0; } if (x + w > cols) w = cols - x; setBufPos(x,y); setAttr(attr); int len = s.length(); if (w < len) wtr.write(s.substring(0,w)); else { wtr.write(s); if (w > len) fill(x+len,y,w - len,1,' '); } } public void beep(int type) throws IOException { byte[] b = null; if (type == 0) b = ti.getStr(TI.BELL); else b = ti.getStr(TI.FLASH_SCREEN); if (b == null) b = new byte[] { (byte)7 }; putp(b); } private void putp(int i) throws IOException { byte[] s = ti.getStr(i); if (s != null) ti.tputs(out,s,1); } private void putp(byte[] s) throws IOException { ti.tputs(out,s,1); } private void move(int orow,int ocol,int row,int col) throws IOException { if (col == 0) { if (row == 0 && cursor_home != null) { ti.tputs(out,cursor_home,1); return; } else { if (carriage_return != null) ti.tputs(out,carriage_return,1); else out.write('\r'); ocol = 0; } } if (orow == row) { if (ocol == col) return; if (column_address != null) { ti.tputs(out,ti.tparm(column_address,new int[] { col }),1); return; } int diff = col - ocol; if (diff > 0) { if (cursor_right != null && diff <= 4) { while (diff-- > 0) putp(cursor_right); return; } } else if (cursor_left != null && -diff <= 4) { while (diff++ < 0) putp(cursor_left); return; } } if (ocol == col) { int diff = row - orow; if (diff > 0) { if (cursor_down != null && diff <= 4) { while (diff-- > 0) ti.tputs(out,cursor_down,1); return; } } } if (cursor_address != null) { ti.tputs(out,ti.tparm(cursor_address,new int[] { row, col }),1); return; } } public TermInfo getTermInfo() { return ti; } public Keyboard getKeyboard() { return keyb; } public void close() throws IOException { setBufPos(0,rows-1); vidattr(TI.A_NORMAL); putp(TI.CLR_EOL); putp(TI.EXIT_CA_MODE); out.flush(); out.close(); } }