/* * DumbTTY.java - A Console Applet by Chuck McManis. * * Copyright (c) 1996 Chuck McManis, All Rights Reserved. * * Permission to use, copy, modify, and distribute this software * and its documentation for NON-COMMERCIAL purposes and without * fee is hereby granted provided that this copyright notice * appears in all copies. * * CHUCK MCMANIS MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE * SUITABILITY OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. CHUCK MCMANIS * SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT * OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. */ import java.applet.Applet; import util.comm.DataChannelInputStream; import util.comm.DataChannelOutputStream; import java.awt.Graphics; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Color; import java.awt.Dimension; import java.io.IOException; import java.awt.Image; import java.awt.Event; /** * This applet class implements a pretty "dumb" terminal. * * The purpose of this applet is to allow one to demonstrate * Java applications that have been written to talk to a * console, It works by creating a couple of DataChannels * to provide the communication mechanism. */ public class DumbTTY extends Applet implements Runnable { char screenBuffer[]; char altBuffer[]; Font fnt = new Font("Courier", Font.PLAIN, 16); FontMetrics fm; DataChannelInputStream scrn; DataChannelOutputStream kbd; int line, oneChar; Thread monIO; Thread blinker; // Terminal characteristics, default is 80 x 24 int pages = 1; int totalLines = 24; int totalColumns = 80; int onePage = 1920; // The "name" of this tty. String ttyName; int minCursor; int displayCursor; int cursor = 0; int newCursor = -1; private int getInteger(String p, int d) { try { return Integer.parseInt(getParameter(p)); } catch (Exception e) { return d; } } private String getString(String p, String d) { String r = getParameter(p); return (r == null) ? d : r; } int row() { return ((cursor - ((pages - 1) * onePage))/totalColumns); } int col() { return (cursor % totalColumns); } /** * Return the cursor value for row 'r' and column 'c'. Before * computing it r and c are constrained to be a value 0 and their * maximum in totalLines and totalColumns respectively. */ int cursorFor(int r, int c) { int rx = Math.min(Math.max(r, 0), totalLines - 1); int cx = Math.min(Math.max(c, 0), totalColumns-1); return minCursor + (rx * totalColumns) + cx; } /** * This draws a raised rectangle look of varying thickness. */ void rect3D(Graphics g, int x, int y, int w, int h, int thick, boolean raised) { h--; w--; g.setColor(raised ? dkColor : brColor); for (int i = 0; i < thick; i++) { g.drawLine(x+i, y+h - i, x+w - i, y+h -i); g.drawLine(x+w - i, y+h - i, x+w - i, y+i); } g.setColor(raised ? brColor : dkColor); for (int i = 0; i < thick; i++) { g.drawLine(i+x, y+h - i, x+i, y+i); g.drawLine(i+x, y+i, x+w - i, y+i); } } int myBorder = 16; Color bgColor = new Color(255, 255, 240); Color dkColor = (Color.gray).darker(); Color brColor = (Color.gray).brighter(); /** * Initialize this applet. */ public void init() { String s; ttyName = getString("ttyname", "console"); totalLines = getInteger("rows", 24); totalColumns = getInteger("columns", 80); pages = getInteger("pagebuffer", 3); onePage = totalLines * totalColumns; screenBuffer = new char[onePage * pages]; altBuffer = new char[onePage * pages]; minCursor = ((pages - 1) * onePage); displayCursor = minCursor; cursor = minCursor; clear(); monIO = new Thread(this); monIO.setName("DumbTTY I/O Monitor"); scrn = new DataChannelInputStream(ttyName + ".screen"); kbd = new DataChannelOutputStream(ttyName + ".keyboard"); selectFont(getSize(), myBorder); monIO.start(); } void selectFont(Dimension x, int border) { int fSize = 32; FontMetrics f; do { fnt = new Font("Courier", Font.PLAIN, fSize); fm = getFontMetrics(fnt); line = fm.getHeight(); oneChar = fm.stringWidth("0"); if (((line * totalLines) <= (x.height - border*2)) && ((oneChar * totalColumns) <= (x.width - border*2))) { termW = oneChar * totalColumns; termH = line * totalLines; return; } fSize--; } while (fSize > 5); } int termW, termH; // Terminal dimensions. /** * Completely draw the screen. This fills the entire * screen with text. */ public void paint(Graphics g) { int w = getSize().width, h = getSize().height; int dx = (w - termW) / 2; int dy = (h - termH) / 2; g.setFont(fnt); g.setColor(bgColor); g.fillRect(0, 0, w, h); g.setColor(Color.gray); g.fillRect(dx-16, dy-16, termW+32, termH+32); rect3D(g, dx-16, dy-16, termW+32, termH+32, 4, true); rect3D(g, dx-8, dy-8,termW+16, termH+16, 4, false); g.setColor(Color.black); g.fillRect(dx-4, dy-4, termW+7, termH+7); g.setColor(Color.green); for (int i = 0; i < totalLines; i ++) { g.drawChars(screenBuffer, displayCursor + (i * totalColumns), totalColumns, dx , dy+fm.getMaxAscent()+(i * line)); } g.setColor(Color.orange); g.drawLine((col() * oneChar)+dx, row() * line + dy + fm.getAscent(), (col()+1) * oneChar+dx, row() * line + dy + fm.getAscent()); g.drawLine((col() * oneChar)+dx, row() * line + dy + fm.getAscent() + 1, (col()+1) * oneChar+dx, row() * line + dy + fm.getAscent() + 1); } private Image altImage; private Graphics offscreen; public void update(Graphics g) { if (offscreen == null) { altImage = createImage(getSize().width, getSize().height); offscreen = altImage.getGraphics(); offscreen.setFont(fnt); } paint(offscreen); g.drawImage(altImage, 0, 0, null); } private void clear() { for (int i = 0; i < screenBuffer.length; i++) screenBuffer[i] = ' '; } public void clrScrn() { clear(); newCursor = 0; repaint(); } /** * Add one character to the display. This is the method that * interprets the character stream being sent to the TTY and * causes internal or external actions to occur. Currently * the supported characters are only ^H for backspace, ^I for * tab to next 8 character column, and ^M and ^J for cursor * to the next line versus cursor to the head of the line. */ public synchronized void addChar(char x) { switch (x) { case '\n' : if (row() == (totalLines - 1)) scroll(); else cursor += totalColumns; // fall through to CR, thus we do a on receipt of case '\r' : cursor = cursor - col(); break; case 0x8: if ((cursor % totalColumns) != 0) cursor--; break; case 0x9: // ^I cursor = (cursor - (cursor & 0x7)) + 8; break; default : try { screenBuffer[cursor] = x; } catch (Throwable e) { System.out.println("Cursor was bogus = "+cursor); scroll(); } cursor = (cursor + 1); if ((row() == (totalLines - 1)) && (col() == (totalColumns-1))) { scroll(); cursor = (pages * onePage) - totalColumns; } break; } repaint(); } public boolean keyDown(Event e, int key) { try { kbd.write((byte) key); kbd.flush(); } catch (java.io.IOException es) { } addChar((char) key); return true; } /** * This method scrolls the text in the page buffers. It does this * by using an alternate buffer and swapping them before displaying. * * Lines that scroll off the top or bottom of the buffer are lost, * new lines appear at the "other" end of the buffer and are filled * with spaces. * */ public synchronized void scroll(int n) { int an = Math.abs(n); if ((an == 0) || (an >= (onePage * pages)/totalColumns)) return; for (int i = 0; i < altBuffer.length; i++) { altBuffer[i] = ' '; } if (n > 0) { System.arraycopy(screenBuffer, n*totalColumns, altBuffer, 0, (onePage * pages) - (n*totalColumns)); } else { System.arraycopy(screenBuffer, 0, altBuffer, n*totalColumns, (onePage * pages) - (n*totalColumns)); } char tmp[] = screenBuffer; screenBuffer = altBuffer; altBuffer = tmp; repaint(); } /** * Scroll the screen up one line. */ public void scroll() { scroll(1); } /* ******** * Screen Communications * * The applet starts a separate thread to monitor I/O * on its "input" channel. This is effectively the input * side of its serial port. Then uses keypresses from * the applet as its "output" side. * */ boolean wasSuspended = false; /** * Stopping the applet just suspends the monitor thread. */ public void stop() { monIO.suspend(); wasSuspended = true; } /** * Starting the applet resumes the monitor thread. */ public void start() { if (wasSuspended) monIO.resume(); } /** * When the applet is destroyed, the monitor thread exits. */ public void destroy() { monIO = null; } /** * This is where part of the magic occurs. Our terminal applet * has a thread that monitors an input stream for data. When it * arrives it adds it to the stuff already on the screen and then * calls repaint. */ public void run() { byte buf[] = new byte[256]; int c; /* * This loop then is the "output" side of the terminal. * It reads the DataChannelInput stream assigned to * .screen and feeds all of the characters * it gets out to the screen. Note that since the screen * is inherently "byte" oriented, it cannot display UNICODE * characters, but what do you want with "dumb" terminal * anyway? */ while (Thread.currentThread() == monIO) { try { c = scrn.read(); // read (block) on first byte. if (c == -1) { System.out.println("EOF on DataChannel!"); break; } addChar((char) c); for (int a = scrn.avail(); a > 0; a = scrn.avail()) { if (a < buf.length) { c = scrn.read(buf, 0, a); } else { c = scrn.read(buf); } for (int i = 0; i < c; i++) addChar((char) buf[i]); } } catch (IOException e) { break; } repaint(); } } }