Per's DomTerm blog entries

Shell integration refers to a protocol between a terminal emulator and a shell (or other REPL) to provide various features, some shown below. The main idea is that the shell sends extra escape sequences to mark the various parts of a command: prompt, input, and output. Terminals that have supported some variation of this concept include XMLterm, GraphTerm, FinalTerm, Iterm2, ExtraTerm, and DomTerm. These protocols are not consistent and have missing funtionality, so I have been working on a proposed specification.

The rest of this article will show some of the supported features, and discuss some unresolved issues. If you want to try out the current state, please clone DomTerm, and follow the build instructions. Best results (and the following screenshots) use the current gitgub master of Fish, after sourcing tools/shell-integration.fish (in the DomTerm sources).

Visually separate commands from each other

Shell integration enables each command (with its associated input and output) to be separated from other commands. The default DomTerm styling uses a thin partially-transparent green line you can see in the screenshot below colors1.png

Different styling for prompt, input, and output

It is helpful to use a different styling for input and output. A simple version is to add distinctive styling for the prompt, but adding styling for entire input lines is even better. The DomTerm default styling (for light mode) is a pale green background for the prompt, a light yellow for the actual input, and a slighly darker yellow for the rest of the input line. Using pale background colors makes it less likely that it will conflict with user color preferences, including syntax coloring.

colors2.png

Note that the actual user input are distictively styled, even spaces. It is helpful to see what are the actual input characters, to avoid surprises in selection or cursor movement. The above echo -n foo \ illustrates this - the input line ends with two spaces: The final space is removed by the fish parser, but the space before is escaped by a backslash, as you can see in the output. (The down-arrow symbol is emitted by fish to indicate a missing final newline in the output. The output ends in two spaces: one following foo in the input and one following the backspash.)

Button to hide/show command output

One useful feature enabled by shell integration is a button to hide (or show) the output of a command. DomTerm puts a downward triangle in the right gutter of the first line if there is any output or more than one input line. Clicking on the button hides all but the first line, and changes the triangle to a right-pointing one (that can be clicked to restore the hidden lines).

hidden1.png

DomTerm places the hide/show button in the same right gutter that is also used to mark wrapped long lines (as shown above). The same area is also used to show error status (below), and for the cursor when at the end of a line.

Show error status

When a command finishes, the application can send the exit status in an escape sequence to the terminal. DomTerm handles a non-zero exit status by displaying a red circle in the right gutter of the last line of the command. (If there is no output, as with the false command, the circle appears in the last input line.) Hovering over the red circle displays the exit code.

errors1.png

Move cursor using mouse

People new to shells usually expect that they can move the input cursor using a mouse click, and are surprised when it doesn't work. Experienced shell users also sometimes want this ability, for example making an edit to a long command. This can be implemented using xterm-style mouse handling, but that is non-trivial to implement. It also has some undesirable side-effects, including disabling the default mouse behavior for selection and for context menus.

A simpler solution is to configure a terminal emulator to map a mouse click to the appropriate number of cursor-motion keystrokes. By default, the terminal can send the escape sequences emitted for the Left and Right arrow keys. This works well for many unmodified REPL applications. (However, to avoid surprises, this has to be specifically enabled by adding a special escape sequence to the prompt.)

DomTerm also allows you to force mouse-click translation: If the Alt modifier is pressed during a left mouse-click, the DomTerm sends the appropriate arrow-key sequence to move the cursor, even if translation has not been eplicitly requested. This is useful for an application like vim, which understands arrow keys but does not have mouse handling by default.

Motion is easy to handle when moving within a single line. Some input editors (such as fish_ support multi-line input areas. Things get more complicated when the mouse-click is a different line than the current position. When the terminal calculates the arrow-key sequence it must be able to ignore prompts and non-significant spacing, such as the indentation fish emits, or the space before any right-edge prompt. Another issue is what kind cursor motion the terminal should send: For fish you only want to send Left/Right arrows, since Up/Down moves through the history. The draft protocol and the DomTerm implementation support multiple options.

Integrating the selection and clipboard

How to integrate the system selection and clipboard with the input editor raises a number of questions that deserve a separate article. The way Emacs integrates the "region" with the selection seems a good approach (but the devil is in the details).

In DomTerm, if you make a selection that ends inside the input area, then DomTerm moves the cursor to the end of the selection (in the same way as if you clicked):

sel1.png

If you type Shift-Right or Shift-Left (optionally with Ctrl) the selection is extended and the cursor moved to the new endpoint. For example, typing Ctrl-Shift-Right extends the selection by one word:

sel2.png

Typing Ctrl-Shift-X copies the selection to the clipboard, and deletes it by emulating the appropriate number of Delete or Backspace key-presses.

Coming soon is having Delete or Backspace delete the selection, without copying to the clipboard.

Re-flow on window re-size

Many terminals will automatically re-flow (re-break) wrapped lines when the window width is changed. The terminal will also send a notification to the application, which may also re-display the output to take into account the changed size. The re-display done by the terminal may be conflict with that done by the application. For fullscreen applications it's not a problem because the applications can clear the buffer and re-paint, which clear any conflicting re-flow done by the terminal. For shells it is trickier: Suppose the current input fits on one line, but the window is made narrower. The terminal re-wraps the line as two lines. Then the application gets the window-changed notification, and redisplays the input line - but without taking into account the terminal's re-wrap, making a mess of the display.

This is a hard problem: Having the application assume re-wrap has happened is unreliable, because another re-size and re-wrap may have happened in the meanwhile. It is better to disable terminal re-wrap for the current line (the one containing the cursor), but that breaks in the case of multi-line input if the application re-sends previous input lines.

The proposed specification recommends that on re-size the application re-sends a prompt-start escape sequence, but not the command-start escape sequence. The terminal can if needed move the cursor to the first input line when it sees the prompt-start sequence. This seems to work pretty well.

(Better would be to let the terminal can handle the re-wrap, and the application not do re-display at all. This avoids the race condition, and it makes old input lines wrap the same way as the current line. However, that may require more radical protocol changes: It works best if the application can act as if the available width is unlimited, and delegate line-breaking it to the terminals.)

Select only real user input

When selecting and copying text that includes input it is usually more useful to ignore indentation, prompts, and extra spacing (such as between user input and a right prompt).

Right and continution prompts

Some shells and input libraries (including fish and JLine3) let the application specify a right prompt displayed at the right of the line. This may complicate selection/copy, as you may not want to include the right prompt (especially in the middle of multi-line input), nor do you want selection to include the spacing before the prompt. Ideally, reflow after width change should adjust the spacing so the prompt of old input lines stays at the right. (This is not yet implemented.) Fish will only display the right prompt if there enough space; ideally this should be taken account on reflow.
Created 14 Jun 2019 16:27 PDT. Last edited 16 Oct 2019 13:03 PDT. Tags:

Using web technologies for the GUI of a desktop application makes a lot of sense: they're portable, familiar, and powerful. A popular option is to use a comprehensive framework such as Electron. However, you might want the option of using a regular desktop browser — maybe you want to be able to access your application remotely, or you might prefer a lighter-weight embedded browser such as webview.

For a native-looking application you probably want menus: a top menubar as well as a pop-up (context) menu. While the various embedded platforms (such as Electron) each have their own APIs for menus, there is no standard JavaScript API. The DomTerm terminal emulator can be run with a small Electron wrapper (in which case it uses Electron menus), but it can also run in a plain browser like Firefox or Chrome. So I looked around for a lightweight menu library that would work in a desktop browser, but I didn't find anything I liked: They were unmaintained, or too big, or depended on some other library (such as jQuery), or were incomplete (popup menus but no menubar support, or missing keyboard navigation), or had an API too different from Electron's relatively simple one.

I finally found Sam Wray's nwjs-menu-browser, which satisfied most of the goals. It was strandalone, modest size, handled menubars as well as popups, and had an API similar to NW.js (which is similar to that of Electron). However, it had some issues. Missing features included keyboard navigation, inability to share menu-items between menus, and some smaller problems. I also found the build process too complicated for me.

The jsMenus library is a much-changed fork of nwjs-menu-browser. The functionality (and API) are very similar to that of Electron. In fact DomTerm uses much of the same JavaScript to construct menus whether using Electron or jsMenus. To see most of the features in action, check this live demo. It should be easy to figure out how it works from the demo.html source. You can also read the API documentation.

Finally, here is screenshot of jsMenus in action. This is DomTerm, using the Chrome browser. Chrome was started with the --app command-line option, which (among other things) disables the default menubar and location bar.

domterm-context-menu.png
Created 29 Jul 2018 14:09 PDT. Last edited 29 Jul 2018 16:11 PDT. Tags:

The DomTerm terminal emulator now supports sub-windows (panes) and tabs. These are resizable and draggable. This is implemented using the GoldenLayout JavaScript library.

The screenshot below shows 3 sub-windows. The lower one has two tabs, domterm-2 and domterm-3. The latter has focus (indicated by the magenta border), and is running the mc file manager. The upper left pane (domterm-1) shows how to embed html from the shell. The domterm-4 pane shows using Kawa to create composable picture values which are displayed using embedded SVG.

You can resize panes by dragging the separator between them, and you can re-arrange panes or tabs by dragging a title bar. The screenshot shows as we are dragging the splitter between the two upper panes; the blue rectangles temporarily display the resulting sizes.

domterm-panes-2.png

Compared to GNU screen or tmux, DomTerm supports more flexible layouts and easier manipulation using either mouse or keyboard. However, DomTerm does not yet support sessions that can be detached or accessed by multiple users at once, though I do hope to add it. Until then, I suggest using abduco or dtach to handle session management.

Created 5 Jul 2017 20:46 PDT. Last edited 6 Jul 2017 10:18 PDT. Tags:

The DomTerm terminal emulator has a number of unique features. In this article we will explore how it enables dynamic re-flow for Lisp-style pretty-printing.

The goal of pretty-printing is to split a text into lines with appropriate indentation, in a way that conforms to the logical structure of the text.

For example if we need to print the following list:

((alpha-1 alpha-2 alpha-3) (beta-1 beta-2 beta-3 beta-4))
in a window 40 characters wide, we want:
((alpha-1 alpha-2 alpha-3)
 (beta-1 beta-2 beta-3 beta-4))
but not:
((alpha-1 alpha-2 alpha-3) (beta-1
 beta-2 beta-3 beta-4))

Pretty-printing is common in Lisp environments to display complicated nested data structures. Traditionally, it is done by the programming-language runtime, based on a given line width. However, the line width of a console can change if the user resizes the window or changes font size. In that case, previously-emitted pretty-printed lines will quickly become ugly: If the line-width decreases, the breaks will be all wrong and the text hard to read; if the line-width increases we may be using more lines than necessary.

Modern terminal emulators do dumb line-breaking: Splitting a long lines into screen lines, but regardless of structure or even word boundaries. Some emulators remember for each line whether an overflow happened, or whether a hard newline was printed. Some terminal emulators (for example Gnome Terminal) will use this to re-do the splitting when a window is re-sized. However, that does not help with pretty-printer output.

Until now. Below is a screenshot from Kawa running in DomTerm at 80 colutions.
pprint-1.png

We reduce the window size to 50 columns. The user input (yellow background) is raw text, so its line is split non-pretty, but the output (white background) gets pretty re-splitting. (Note the window size indicator in the lower-right.)
pprint-2.png

We reduce the window size to 35 columns:
pprint-3.png

It also works with saved pages

DomTerm allows you to save the current session as a static HTML page. If the needed DomTerm CSS and JavaScript files are provided in the hlib directory, then dynamic line-breaking happens even for saved log files. (The lazy way is to create hlib as a symbolic link to the hlib directory of the DomTerm distribution.)

Try it yourself on a saved session. The --debug-print-expr flag causes Kawa to print out each command before it is compiled and evaluated. The result (shown in red because it is sent to the standard error stream) is pretty-printed dynamically.

Structured text

This is how it works.

When an application pretty-prints a structure, it calls special output procedures to mark which parts of the output logically belong together (a logical block), and where line-breaks and indentation may be inserted. In Kawa the default print formatting for lists and vectors automatically calls these procedures when the output is a pretty-printing stream. The pretty-printing library calculates where to put line-breaks and indentation, based on these commands and the specified line length.

However, when the output stream is a DomTerm terminal, Kawa's pretty-printing library does not actually calculate the line-breaks. Instead it encodes the above-mentioned procedure calls as special escape sequences that get sent to DomTerm.

When DomTerm receives these escape sequences, it builds a nested DOM structure that corresponds to the orginal procedure calls. DomTerm calculates how to layout that structure using a variant of the Common Lisp pretty-printing algorithm, inserting soft left-breaks and indentation as needed.

When DomTerm detects that its window has been re-sized or zoomed, it first removes old soft line-breaks and identation. It does re-runs the layout algorithm.

When a page is saved, the nested DOM structure is written out too. If the saved page is loaded in a browser, and the necessary JavaScript libraries are available, then the pretty-printing algorithm is run on the saved page, both on initial load, and whenever the window is re-sized.

Hyphenation and word-breaking

DomTerm supports general soft (optional) line breaks: You can specify separate texts (optionally with styling) for each of the following: The text used when the line is not broken at that point; the text to use before the line-break, when broken (commonly a hyphen); the text to use after the line-break (following indentation). One use for this words that change their spelling when hyphenated, as may happen in German. For example the word backen becomes bak-ken. You can handle this using ck as the non-break text; k- as the pre-break text; and k as the post-break text.
Created 30 Jan 2017 13:23 PST. Last edited 3 Feb 2017 12:13 PST. Tags:

I have recently spent a lot of time on DomTerm, which is becoming a fairly decent terminal emulator. Being based on HTML5 technologies lets you do many interesting things, including embed graphics. The new qtdomterm standalone terminal emulator is especially nice; alternatively, you can also use the domterm --browser command to start a process that uses a window/tab in your default desktop browser.

The gnuplot package is a powerful graphing package. It has a command-line that lets you specify complicated functions and plots, and then give a plot command to display the resulting plot. Unfortunately, it is difficult to find a a good front-end that is usable for both command-line interaction and graphics. People usually have to specify output to either a file or an external viewer. Would that one could insert the plots directly in the REPL output stream!

The development version of gnuplot (i.e. 5.1, available from CVS) has native support for DomTerm. It has domterm as a new terminal type, which you can select explicitly (with the command set term domterm), or use by default (since gnuplot checks the DOMTERM environment variable, which is set by DomTerm).

This works by using the pre-existing svg output driver to generate SVG, but surrounding the SVG by escape sequences, and then printing the result to standard output. DomTerm recognizes the escape sequence, extracts the SVG (or more generally clean HTML) and inserts it at the cursor.

You can save the session output to an html file. Here is an example. In qtdomterm you can use the File / Save As menu item; otherwise ctrl-shift-S should work. This is a single html file, with embedded SVG; images are embedded with a data: URL. The file is actually XML (xhtml), to make it easier to process. The saved file does need to css and js (JavaScript) files to be readable, so you need to link or copy the hlib directory in the DomTerm source distribution.

1.png

Created 18 Aug 2016 20:45 PDT. Last edited 21 Sep 2016 23:06 PDT. Tags:
Tags: