High level functional description of a Terminal Emulator, Term

# High level functional description of an ANSI Terminal Emulator, Term

Author:

Ivan Soleimanipour, Sun Microsystems.

Abstract: A highly configurable ANSI terminal emulator.
Version: 4 of Wed Oct 24 2001

Table of contents:

- Motivation
- User visible and API Features (as implemented currently)
- Covenant
- RFE queue
- Performance
- Use as a hypertext widget
- Design issues
- Source code and demos
  - Build tool demo
  - Telnet demo

## Motivation

The original impetus for Term was the need for a terminal emulator for use by adaptations of NetBeans for non-Java development. These tools need a proper terminal emulator in at least three areas:

- A window for program io, for programs being run under the debugger. These programs will be traditional unix programs and may utilize various levels of terminal manipulation, programs like 'more', 'top', 'vi' and 'ksh'.
- A window for the debugger (e.g. dbx) command io. The dbx command interpreter is a ksh variant and as such allows input history editing or execution of any unix program (like 'vi').
- A window for running alternative text editors, 'vi' or 'vim' under. More on this later.

The NetBeans community had expressed interest in such a component as a candidate for the NetBeans build and other output windows. The package was contributed to the NB source base circa Aug 2001.

A terminal emulator can also be used as a within-nb shell or telnet window. See the source code for a telnet demo.

## User visible and API Features (as implemented currently)

- The base Term class handles "dumb" operations, putting characters on a screen and forwarding keystrokes.

- ANSI operations for cursor control, line manipulation etc.
- ANSI color and other character attribute escape processing.
  Not All sequences are implemented yet.

  As references I used Robert Frees spec and this as well as the DtTerm(3) man page from Solaris.

- The interpretation of characters is done by an abstract interpreter. A set of classes are provided for interpreter construction using state machines. The current default interpreter is a simple ANSI interpreter, polluted with some DtTerm extensions.

- Selection service in character, word and line modes modelled on xterm with configurable word boundary detection.
  This should work particularly well under X-windows with JDK1.4 where they have fixed X-windows selections.
- Auto-scrolling for the selection when the mouse moves outside of the view.

- History buffer and vertical scrolling. The history can be traversed using a ...
- LogicalLineVistor
  Provides access to whole history buffer contents and facilitates searches and contents dumps etc. Suitable information and mechanisms are provided for mapping a logical line to screen coordinates to facilitate highlighting and resumption of search for example.

- Horizontal scrolling.

- Pluggable "line discipline".

  Unlike other termulators Term does not do things like line buffering, echoing, CR/LF processing and so on. Since it is predominantly intended for use under Unix and since under Unix all this stuff is done by the kernel (accessed through pty's) Term doesn't bother with any of this.

  However, there is a mechanism analogous to SVR4 io streams, or pushable network stack elements, that allows customization of character processing on other systems. A generic simple stream module, LineDiscipline is provided in this package.

  Stream modules can be used for other purposes like recording of the io to some history file or for testing purposes.

- Mechanisms for dealing with resizing for interaction with ptys and telnet protocols.

- "editor and glyph support". The original Solaris Workshop used an enhanced DtTerm (The CDE termulator widget) that provided for improved interaction with vi and the rest of the IDE. So does Term:
  - Ability to render glyphs in a "glyph gutter".
  - Ability to convert mouse clicks to row/col coordinates and even the underlying text. For use by "balloon evaluation" for example.
  - Colored per-line background stripes, independent of ANSI color escapes and the selection feedback.

- Term interacts with the rest of the world using functions that send and receive char arrays. A subclass, StreamTerm allows hooking up of java.io Streams. Other subclasses could be derived which connect the Term to telnet clients or pty streams (which naturally requires JNI code and is therefore not part of this package).

- Choice of click-to-type or follow-mouse focus.
- Choice of XOR or Swing style selection feedback.

- Handles Sun keyboard Cut, Copy, Paste keys. Since they _are_ available as AWT key codes.

- Methods for querying and setting cursor location.
  This can be used, for example, to ensure that a horizontal line starts on column 0.

- Consuming key events.
  Because of it's nature, Term, by default, consumes all of the keystrokes it gets and passes them through to the output stream.

  A set of KeyStrokes may be registered with Term though to allow passing through of keystrokes of the clients choice.

Note that there are two important differences between this widget and traditional Java text widgets:

- Because of the focus on cursor addressability it only works with fixed-width fonts. It will accept any font setting but will mutate it into a monospaced one.
  Although there is some discussion to relax this considering the dearth of monospaced unicode fonts.

- It doesn't view documents; it's an input stream viewer. This leads to some interesting design issues .

The UI ... is just too simple to be worth a picture.
It's just a screen area where text appears and a Swing scrollbar in whatever is the current L&F.

This proposal comes with Javadoc API documentation and a properties table .

## Covenant

(A covenant is a part of a property deed that puts restrictions on the use of the property on all future owners)

- No additional dependencies. I would like Term to be usable outside of NetBeans and therefore it should not depend on packages like openide and so on.

## RFE queue

These are things that are planned but not implemented yet.

- Misc. terminal things like ...
  - Control of cursor style.
  - Bell: audible/visible.
  - Margin warning.

- Resize tooltip so you can get your columns and rows just they way you want them.

- Line re-breaking.
  You average terminal emulator will keep line breaks (due to wrapping) at the original column when the width of the emulator changes. It should be possible to re-break them as the width changes.

- Accessibility.
  This used to be an "issue" mainly because I hadn't learned much about it. Term will implement the Accessible interface. The class Accessible is really just a front gate to a whole host of other stuff that needs to be implemented.

## Performance

Term is made up of the two main subsystems:

Buffer maintenance
  This is the process of processing input characters and adding them to the buffer, advancing the position of the cursor, adding new lines at the bottom and taking away lines at the top.

  The information is stored in a Vector of Lines. Each Line has a an array of 'char's for character storage and an Array of 'int's for attribute storage. These two arrays grow together (which is more efficient than having two separate Vectors or StringBuffers). However the attribute array may be null if all the characters in the line have no attributes.

  In order to prevent needless copying, these buffers are directly available to the paint functions.

Painting
  This is he process of drawing the view of the buffer unto a canvas. On each refresh the _whole_ view is redrawn! Since Swing's refresh manager already provides a double buffered image there is no user visible flickering.

  Lines w/o attributes are rendered using Graphics.drawChars() in the double buffered image setup by default by Swing and AWT.

  Lines with attributes are rendered a "run" at a time. A run is a sequence of characters all of which have the same attributes.

  There are also various adornments to be rendered, like the cursor, slection highlight, glyphs and so on.

The following points may be made:

- Redrawing the whole screen at the slightest excuse seems like a bad idea. But it's premature to do anyting about this yet since Term interaction with JScrollPane has not yet been finalized.
- Regardlass of the the use of runs, use of attributes will always have a slightly higher overhead in both buffer memory use and painting time.

When measuring or comparing performance attention should be paid to the following:

Buffering
  A repaint request is posted on every call to Term.putChar() and every call to Term.putChars(). Obviously things are going to go through much faster if you buffer 1000 characters and send them via putChars() instead of calling putChar() 1000 times. The first implementation of LineDiscipline() actually made this mistake.
Batching of refreshes
  The Swing (AWT?) repaint manager has it's own batching which will compensate a bit for a high frequency of repaint requests.
Jump scrolling
  If you 'cat' a large file into a termulator you can have you choice of two extremes, seeing every line go by and waiting for it all to go by or having the printing finish quickly and seeing "jumps" in the output (while the history buffer retains everything).

  These two alternatives are usually controlled by a property known as "jump scrolling". Term doesn't have it but the effect can be achieved by appropriately buffering the input and or internally using paintImmediately().

Having said all of this, in my experience with the current implementation, painting completely dominates buffer maintenance, so when you're comparing Term with something else make sure they have comparable refreshes/bytes-of-input.

## Use as a hypertext widget

Term has facilities that allow it to be used as a reasonable styled language and hypertext widget.

- Styled text can be rendered using ANSI color and font escapes, or method calls, like Term.setCharacterAttribute(). One can even concieve of an Intepreter that interprets HTML.

- An "Active Region" can be delineated through extended character escapes or method calls (Term.getRegionManager().beginRegion()). Active regions may be nested and the region manager can map a given cursor (or pointer) position to the regions. This is the minimal functionlaity with which a client can associate user data and actions with active regions and highlight a region when a pointer passes over it, execute an action on a click, or pop up a menu.

- History can be "anchored" so that the beginning of a "page" of hypertext doesn't vanish even if it's length exceeds the history size. In other words, when anchored, the history is temporarily expanded to accomodate the page.

## Design issues

I've been working off of O'Reilly publishers "JAVA Swing" by Eckstein, Loy and Wood. Many of the issues here are in the context of that book.

The issues often have to do with my ignorance or lack of good guidelines on a subject and are really a plea for suggestions, education and discussion.

Swing Model and UI Delegate
  In ch 28 it is said that a proper swing widget has to have a Model and a UI Delegate. Term currently has neither.

  Why not a UIDelegate?
    Term used to have no "artwork" that needed to be drawn depending on a specific look and feel. But now it has at least the following properties that could be construed as L&F specific:

    selectionXOR
      You can control whether selections are done using Swing style (background highlighting) or using the more traditional XOR style. One could couch this as a L&F controllable setting with the XOR style going to the Motif L&F.

    autoCopy
      On unix selections usually go to the primary selection. On windows they go into the clipboard and only if a Copy action was requested.

    clickToType
      Windows users live in the ClickToType (focus) world. Unix people (i expect) prefer the "follow mouse" style of focus control.

    Also, term is a "composite" widget and it is unclear to me how one implements UIDelegates for composites, or whether one is even needed.

  Why not a Model?

  In any text rendering widget the model is typically a "document". The equivalent of this in a termulator would be the history buffer, or if no history, just the screen backing storage.

  However, this is really an internal implementation and a terminal really interacts with the external world through it's io lines, so the model is conceptually a "connection".

  But a connection doesn't fit in with things like model-changed events and such ... the data stored in the screen backing store is ephemeral, it soon winks out of the history buffer. So, I"m not exactly sure what a model would be for a termulator.

  One candidate is the concept of sessions (A concept used in the KDE 'konsole') where a single widget can multiplex between multiple connections and the corresponding history buffers. While such a feature should definitely be on the rfe list, again, I'm not sure whether the Swing concept of a Model is appropriate here.
  To that end the current implementation has a Document class used internally, but there's still a bit to go.

Working with JScrollPane
  Term is a composite and carries it's own scrollbar.
  It is not JScrollPane-friendly.
  Here's why ...

  Initially it seems obvious that it should, like every other good Swing component, implement Scrollable, work properly under a JScrollPane and not contain it's own scrollbar, but this is not neccessarily a good idea.

  Usually the size of a component refers to it's visible size. But when the component is placed under a JScrollPane, it's size starts referring to it's "virtual" size over which the user scrolls. While this makes complete sense for tables, lists, and text documents, in the case of a terminal emulator we don't want this. We want size to still govern the visible screen size!

  It is perhaps possible to design a Term that works under JScrollPane such that resizes communicate the viewport size via TermListener.sizeChanged() and setRows() sets the viewports size (this I actually can't figure how to do). But this requires that Term discover that it's under a JScrollPane and things become progressively more complex from there on.

  Another argument can be made from a utility standpoint.
  Most termulator applications (xterm) and widgets (DtTerm) have a property that governs whether and where they have a scrollbar. It would be much more convenient to retain this as a runtime controllable property of Term itself (read user option) instead of having the container of Term manage that; create a JScrollPane, put Term under it etc. in response to user option toggling.

Reuse of and differences from standard Swing text components
  Why write a text rendering widget from scratch? Following are some reasons. Some of them probably stem more from my ignorance of Swing or the NIH syndrome, but others I believe are fair justifications for a new component.

  - Speed.
    In my experience Swing text components just can't handle the volumes of stuff coming into them that you typically get in consoles.

    I believe this is not so much because of the complex Document and Element structures but due to the same reasons that could make Term slow, and that is control over buffering and batching, or rather lack thereof. Read the section on performance to see what I mean.

  - The selection and the cursor are strongly tied together in Swing. With Term the selection is independent of the cursor.

  - Swing provides a whole infrastructure for Actions, KeyMaps, undoing etc. Which are neccessary for it's functioning as a document "editor". Term, while internally maintaining a document and performing "edits" on it, is not fundamentally an editor and can dispense with all of the above.

  - Swing uses offsets as opposed to cartesian coordinates. Since the bulk of the operations of terminal emulators involves cartesian cursor motion and such, in order to reuse a Swing text component a fair amount of machinery has to be added to convert between the two.
    Some ideas I've tossed out:

    Pad lines with spaces
      ... so every "line" is 80 characters wide, making it easier to do arithmetic. But then selection will absorb those spaces which have to be thrown out etc.
    Track line boundaries
      Using Positions for evey line? Expensive. Tracking edits and maintaining my own map? Exspensive.
    Use "Line" Elements.
      Expensive but more importantly means we forego using Elements for more useful delineations.

  - Swing Elements are very similar to Terms ActiveRegions. They differ in the following respects though:

    - Elements use Positions. While ActiveRegions use Coords. Positions are "marks" and entail a fair amount of overhead, while Coords are like offsets.

      The offsets used by Term are actually valid as regions go out of history. This because "absolute coordinates" are used where each line put into Terms history gets a serial row number which only grows. Term correctly handles the case where the 32-bit absolute row number wraps. At a continuous rate of output of 4000 lines/sec the wrapping will happenin about 4 days.

    - Elements and AttributeSets are tightly bound, while with Term Character attributes are set completely independently of Active Regions.

    - Elements are strictly nested. That is, there is no gap between the beginning of a branch Element and the beginning of it's first child. ActiveRegions can be arranged more flexibly.

  - CaretEvent gets fired everytime the cursor moves. Imagine what this does to massive amounts of text being hurled at a terminal emulator, even if no-one is listening.

  - Swings 'int offset' is "between" characters, while termulators work with coordinates "on" characters. I"m not sure that Position's BIAS would help here. (This is perhaps a red herring since you can adopt the convention that a caret's position implies a cursor on the character following it).

Key tables
  (NOTE: This is not the same as Swings KeyMaps).
  Term currently doesn't have a key map table. Most other termulators do. The thing is I haven't yet seen the need. Lot of the usual type of processing that key maps do seems to be absorbed by Swing and AWT, so my initial hope was to just piggy-back on that. Same thing for byte to char conversions.

  There have been a few glitches like AWT converting a CR to a LF that are currently being worked around in ad-hoc manner.

  ## Source code and demos

The source code for Term is in the NB CVS repository under

    core/libsrc/org/netbeans/lib/terminalemulator.

Two demo's are available:
(Actually they are not! I'm not sure whether it is appropriate to put them into the CVS repository).

### Build tool demo

This is to showcase the capabilities of ActiveTerm and how it can be used to annotate output, say from a build script.

A fair amount of the code has to do with the gui setup, javac Process setup and capture of it's output. Once the output is received it is processed line-by-line. A crude pattern is used to detect the first line of a javac error message and a new ActiveRegion is begun, while the previous one is ended. These active regions have their feedback attribute set which means that they will be highlighted as the mouse moves over them. Anyone who has had to decipher a wrapped compilation line and error line from 'make' would appreciate this.

The sourcefilename and line number are also extracted using simple patterns and put in their own ActiveRegions as HTML-style links, which if clicked will bring up the right line # in some editor, or rather fake it using a popup.

The beginning of each error message is also tagged with a simple glyph in the glyph gutter.

Please keep in mind that this is strictly a Term feature demo. I"m not advocating using glyphs to tag error messasges, or that textual pattern recognition should be used for detecting distinct error messages and their component, nor is capturing of Process output is as trivial as it seems here (if the demo had instead used 'make' you'd see some reordering of output because some comes from stdout and some comes from stderr).

### Telnet demo

For a telnet client it uses portions of The Java(tm) Telnet Application/Applet by Matthias L. Jugel and Marcus Meisner. I haven't included that stuff here to save space but you only need

    de/mud/ternet/ScriptHandler.java
    de/mud/ternet/TelnetProtocolHandler.java
    de/mud/ternet/TelnetWrapper.java

You'll have to tweak the settings in the Makefile and the 'telnet' wrapper script.