SyncTERM is a terminal program written specifically for connecting to Bulleten Board Systems (BBSs). Despite the name, SyncTERM is in no way Synchronet specific, it just happens to share a large portion of code with the rest of the Synchronet project, and live in the same git repository.

Getting Support

If you need help with SyncTERM, the best places are:

IRC

Connect to irc.synchro.net and find Deuce in #Synchronet. Ask your question, then idle. I can take hours to respond. Do not give up, this is the quickest way to get a response.

SourceForge

The official SyncTERM project page has a bug tracker and other features that will email me and provide tracking for issues that are filed.

E-Mail

I am usually fairly responsive to emails sent to me at shurd@sasktel.net. Please describe your issue as clearly as possible.

Dove-Net

While I no longer read Dove-Net regularly, many other users can often help with support issues. Ask questions in the Hardware/Software Help sub. If your local BBS does not carry Dove-Net, you can telnet to vert.synchro.net and leave messages there.

Throughout this document, I will mention things which are not supported. These are things which I don’t normally test, and are unlikely to work at any given time. If you ask for support on one of these issues, I may help out, or I may not. It doesn’t bother me if you ask for help on these things, but if you continue to ask for help after I refuse, it will make it less likely I’ll work on it in the future.

History

I started writing SyncTERM in June of 2004. There weren’t any good BBS clients available for FreeBSD at the time, so I started writing one. Initially, it was RLogin only and no file transfer support existed or was planned. since RLogin supports auto-login with user ID and password on Synchronet systems, and RLogin is a much simpler protocol than telnet, no telnet support was planned. Digital Man (authour of Synchronet) added telnet and ZModem support a year later, and SyncTERM became a generally usable BBS client. New features continued to be added slowly over the years.

Getting SyncTERM

Releases of SyncTERM are available on the SourceForge project page. Nightly builds and source bundles are also available at the SyncTERM website for the more adventurous.

Compiling SyncTERM from Source

Windows users should not need to build SyncTERM from source. Windows specifically is not an easy build to do. The Windows builds are done on a FreeBSD system using MinGW, and this is the only supported build method currently. There is a Visual Studio project for building SyncTERM, but this is not supported.

For *nix systems (Linux, FreeBSD, macOS, and others), a GNU make based build system is used. There are a number of optional dependencies, and a large number of supported compile flags (many of which are shared with Synchronet).

One dependency that, while technically optional really should be included is libjxl. This allows graphics to be supported with good compression rather than the uncompressed PPM that is the only format otherwise.

The biggest (usually) optional dependency is SDL 2. SyncTERM can use SDL for both graphics and sound. X11 and Wayland can also be used for graphics, and OSS, ALSA, or Portaudio can also provide sound. These use run-time linking, so at compile time, only the headers are needed. Static linking with SDL is also supported. SDL2 is required for Haiku at the present time. On macOS, the native Quartz backend and CoreAudio are used by default and SDL is not required.

For SSH, a copy of Peter Gutmann’s Cryptlib is provided along with a set of patches. This is still an optional dependency, so if Cryptlib doesn’t build on your platform, you can still use SyncTERM’s other connection options. Cryptlib must be statically linked if it is used.

Other optional dependencies (such as Portaudio) can not be statically linked, and are only supported with run-time dynamic linking.

Once you have the desired dependencies installed, change to the syncterm directory in the source tree (ie: syncterm-YYYYMMDD/src/syncterm) and run the make RELEASE=1 command. This will generate the binary in a subdirectory with the following name format: [compiler].[os].[arch].exe.[build]

[compiler]

is likely either gcc or clang depending on the system compiler.

[os]

is the OS name reported by uname.

[arch]

is the architecture reported by uname unless it is x86 compatible in which case it is forced to x86 for historical reasons.

[build]

is "release" for release builds and "debug" for debug builds.

SyncTERM can be installed with make RELEASE=1 install

Running SyncTERM

SyncTERM supports many command-line options to control behaviour. Options begin with a - followed by one or more other characters. The following options are supported (options are not case sensitive unless specifically noted):

-6

Specifying -6 forces SyncTERM to use IPv6 addresses when possible.

-4

Specifying -4 forces SyncTERM to use IPv4 addresses when possible.

-B/path/to/bbs/list.lst

Loads the user BBS list from the specified file instead of the default.

-C

Changes the default to no status line.

-E##

Specifies the escape delay in ANSI on Curses modes. The escape delay is how long SyncTERM will wait after an escape key is received from the user to see if it’s a control sequence or a bare Escape press. The units are millisecods, and the default is 25.

-H

Use SSH mode when connecting.

-I[ACFXWS[WF]O[WF]]

Selects the output mode. Not all modes are available in all builds or on all platforms. Legal values are:

A

ANSI output mode. This mode outputs ANSI control sequences to stdout.

C

Curses output mode. For use in *nix terminals.

F

Curses with forced IBM character set. Limited usefulness.

G[WF]

Windows GDI output mode. Uses the Win32 API directly, and is the default for Windows. Additionally, a 'W' or 'F' can be specified to force windowed or full-screen mode respectively.

I

Curses in ASCII mode.

S[WF]

SDL window output mode. Uses the SDL library for output. Additionally, a 'W' or 'F' can be specified to force windowed or full-screen mode respectively.

W

Windows console mode. Windows only mode which uses the system console APIs for output.

Q[WF]

Quartz output mode. macOS only mode which uses the native AppKit/Core Graphics framework for drawing. The default on macOS. Additionally, a 'W' or 'F' can be specified to force windowed or full-screen mode respectively.

X[WF]

X11 output mode. UNIX only mode which directly uses the X11 libraries for drawing. Additionally, a 'W' or 'F' can be specified to force windowed or full-screen mode respectively.

Y[WF]

Wayland output mode. UNIX only mode which uses the native Wayland protocol for drawing. The default where supported. Additionally, a 'W' or 'F' can be specified to force windowed or full-screen mode respectively.

Refer to the Output Modes section for more details.

-L##

Specifies the number of lines on the "screen". Supported values are: 14, 21, 25 (default), 28, 30, 43, 50, and 60. If an unsupported value is used, it will default to 43/50.

-pns_*

Only "supported" on macOS. Ignored.

-N/path/to/config.ini

Loads the configuration from the specified file instead of the default.

-Q

Quiet mode, doesn’t show popups by default.

-R

Use RLogin mode when connecting.

-T

Use Telnet mode when connecting. If an upper-case -T is the only argument passed to SyncTERM however, SyncTERM will output a terminfo entry on stdout then exit. See Installing Terminfo Entry for more details.

-S

Use "safe" mode. This mode attempts to restrict the ability of the user to modify the local drive contents. This has not been exhaustively audited, and should therefore not be trusted.

-v

This option is case sensitive and must the be only option passed to SyncTERM. Causes syncterm to output the version on stdout then exit.

After the options, a full URI, hostname, or dialing directory entry may be specified. Supported URI schemes are: rlogin://, ssh://, telnet://, raw://, shell://, ghost://.

If there is an entry matching the URI, hostname, or entry name, the settings will be loaded from the BBS list, then modified per the command-line arguments.

Advanced Keyboard Controls

The conio library directly implements some advanced keyboard controls. While these are not available in all output modes, they are available everywhere and not just in the menus or in the connected state.

Keyboard Controls
Alt+

Snap the window size to the next smaller integer zoom level

Alt+

Snap the window size to the next larger smaller integer zoom level

Alt+Return

Toggle fullscreen mode

Alt+Home

Centre window on the screen

Alt+drag

Hold Alt and drag the window with the left mouse button to move it on the desktop. Available in the Wayland output mode when the compositor does not provide server-side decorations (i.e. there is no title bar to drag).

In addition, you can directly enter characters either by their "codepage" value or their unicode value. A "codepage" value is an 8-bit value that indicates a character in the current codepage (such as the default CP437).

To enter a codepage value, ensure NumLock is on, and hold down an Alt key, then enter the decimal value on the numeric keypad. Do not enter leading zeros, as that would indicate a unicode value. Once the value is entered, release the Alt key.

A unicode value is the Unicode codepoint number. Only unicode values that map to the current codepage can be passed through, so the majority of values can’t be entered in a given mode.

To enter a unicode value, ensure NumLock is on, and hold down an Alt key, press the 0 key on the numeric keypad, then enter the decimal value on the numeric keypad. Once the value is entered, release the Alt key.

The User InterFaCe

Menus in SyncTERM use a common user interface library named UIFC. This library was originally developed for Synchronet.

The following is the general behaviour of UIFC menus.

Mouse controls
Right-click

Same as pressing ESC (ie: exit menu).

Left-click

Select an item in a menu.

If there is a blank line at the end of the menu, you can select it to insert a new item.

Menus have a standard set of mouse controls. If you click outside of a menu, that menu is usually closed, but in some cases, it may simply become inactive. At the top of each menu is a block which is used to close the menu. If there is help for the menu, there is also a ? button to bring up the help.

If there are more options than fit in the window, there is a scrollbar on the left side.

Left-Drag

Select and copy a region (the copy is made when the button is released).

Middle-click

Paste from PRIMARY selection or clipboard.

Keyboard Controls
Return

Select the currently highlighted option. If there is a blank line at the end of the menu, you can select it to insert a new item.

Escape

Exit the current menu.

Backspace

An alias for Escape.

Ctrl+C

An alias for Escape.

Home

Jump to the beginning of the menu

Move to the previous item in the list

Page Up

Jump up in the menu by one screen.

Page Down

Jump down in the menu by one screen.

End

Jump to the end of the menu

Move to the next item in the list.

F1

Help

F2

Edit

F5

Copy

Ctrl+Insert

An alias for F5

Shift+Delete

Cut

F6

Paste

Shift+Insert

An alias for F6

Insert

Inserts a new item.

+

An alias for Insert

Delete

Delete item at current location

-

An alias for Delete

Any letter or number

Jumps to the next item that has that character earliest in it’s name.

Ctrl+F

Find text in options

Ctrl+G

Repeat last find

The File Browser

SyncTERM uses a shared file browser whenever a file or directory must be picked — for example when uploading a file (Alt+U), loading a font (Alt+F), or naming a capture file (Alt+C).

The browser shows three rows of content inside its window:

Directory and File panes

Two side-by-side lists. The left pane contains subdirectories plus a .. entry (except at the filesystem root), and the right pane contains files that match the current mask. Directories are always listed regardless of the mask.

Info pane

A two-line area below the lists. The first line shows the full name of the item currently highlighted in whichever list has focus; the second line shows its size (or the word directory) and last-modified timestamp. In the multi-file picker it also shows [selected] when the highlighted file is already tagged.

Mask and Path fields

The mask filters the file list (for example *.bin). The path field (shown only when the caller permits typing a path, e.g. Upload) lets you type a directory or filename directly; long paths are truncated from the left with a …​ prefix so the deepest directory stays visible.

The footer holds an OK (or Select) button and a Cancel button. In the multi-file picker there is also a Review button.

Keyboard Controls
Tab / Shift+Tab

Move focus forward or backward between the panes, Mask, Path (if shown), and the footer buttons.

/

Between the directory and file panes these move focus left or right. In the path and mask fields they move the edit cursor. Between the footer buttons they move focus between buttons.

/

In the lists they move the highlight. In the path and mask fields they move focus to the previous or next field.

Return

In a list: open the directory, or commit the highlighted file (or toggle its selection in multi-pick). On a footer button: activate that button.

Space

In the multi-file picker’s file list, toggles the highlighted file’s selection.

Ctrl+A

Multi-file picker only — tags every file currently shown in the file pane. Does not descend into subdirectories. Safe to press repeatedly; files already tagged stay tagged.

Ctrl+Return

Activate OK / Select from anywhere in the dialog, regardless of which field currently has focus.

F2

Multi-file picker only — opens the Review pop-up listing every tagged file. Inside the pop-up, Delete or Return or a left click removes the highlighted or clicked item, and Escape closes the pop-up.

Escape

Cancel the browser.

Mouse Controls

Clicking a list item in the currently-focused pane commits it immediately (or toggles it in multi-pick). Clicking a list item in an unfocused pane just moves focus and the highlight there — click again to commit. Clicking in the mask or path field places the edit cursor at that position. Clicking a footer button both focuses it and activates it.

The multi-file picker’s status line shows F2 Review while the dialog is open. It returns the full path of every tagged file at once, so you can queue uploads from several directories in a single pass.

The Dialing Directory

This is the default startup screen if no BBS is specified on the command-line. At the top is the program name and version, a build date, the current output mode, and the current date and time.

With version numbers, trailing letters indicate pre-release versions. 'a' indicates an alpha build which will have known bugs and/or incomplete features. 'b' indicates a beta build which indicates there are expected to be no known bugs, but it has not received testing. "rcX" is a release candidate where X is a number. These indicate that after some period (usually one to two weeks) of no newly reported bugs, a release will be made.

The output mode is important to make note of when reporting issues, since many bugs only impact one or two output modes. It’s after the build date.

The bottom-most line contains help on the current menu, indicating what options are available in the most recent menu.

There are three areas the user can interact with in the dialing directory. The Directory menu, the SyncTERM Settings menu, and the comment line.

The comment line is directly above the help line at the bottom, and allows a per-BBS comment to be entered.

The Directory

This menu lists all the entries in the two dialing directory files. If you move the bar over one and press <Enter>, it will connect you to the highlighted system as configured in the entry.

In addition to the standard controls, this menu also has some extra keyboard shortcuts.

Ctrl+D

Quick-connect to a URL

Ctrl+E

Edit the selected entry (Alias for F2)

Ctrl+S

Manage sort profiles

< / >

Cycle through sort profiles (previous / next). The current profile name is shown in the directory title bar.

Alt+B

View the scrollback of the last session

Tab

Move to the comment field for the current entry, or the settings menu if there is no current entry.

Back Tab (Shift+Tab)

Move to SyncTERM Settings.

To add a new entry, go to the bottom of the list (by pressing end) and select the blank entry at the bottom. A window will pop up asking for the Name of the entry. This name must not already exist in the personal dialing directory.

Next you will be prompted for the protocol to use. Options include:

RLogin

Uses the historic RFC1282 RLogin protocol without OOB data. This is an obsolete, unencrypted protocol that can allow auto-login, and is 8-bit clean (unlike telnet). It is very simple. Instead of the local username, the users password is sent.

RLogin Reversed

Some RLogin servers that support password auto-login have reversed the remote and local username fields. This allows connecting to these servers.

Telnet

Uses the historic and highly complex telnet protocol. This is an obsolete, unencrypted protocol and is not 8-bit clean and predates TCP/IP. It has been the source of many security vulnerabilities over the fifty years or so it has existed. Historically, it has been the most common way to connect to a remote system as a terminal, so is widely supported.

Raw

A raw 8-bit clean TCP connection. This is often what retro BBSs actually support when they say they support telnet.

SSH

The Secure Shell v2 protocol. This is the modern replacement for both telnet and rlogin, and is widely supported. This is encrypted and performs user and server authentication as part of the protocol instead of inline. SyncTERM supports authenticating with both a password and a public key.

SSH (no auth)

The SSH protocol, but will not send a password or public key. Used for auto-login systems where the user name by itself is sufficient.

Modem

SyncTERM can directly control a modem for making outgoing calls.

Serial

Direct communication with a serial port.

3-wire

Serial, but with only transmit and receive. In this mode, there is no way to detect if the remote has hung up and there is no flow control, so bytes can easily be lost. This is primarily used for communicating with embedded hardware, and not BBSs.

Shell

Runs a shell in the terminal.

MBBS GHost

The MajorBBS 'GHost' protocol.

TelnetS

Telnet over TLS. All the drawbacks of the telnet protocol, but at least it’s encrypted.

Finally, you will be promted for the "address". This is the DNS addres, IP address, serial port, or command to connect to. If the connection will be made over the network, and the name is a valid hostname, it will be auto-filled in this field. To overwrite it, simply start typeing.

Once these three pieces of information are entered, the entry is created and you are returned to the Directory. To further modify the settings, you can press F2 to enter the Edit Directory Entry menu.

Edit Directory Entry

In this menu, you can modify all the connection settings for an entry. The exact contents of this menu will vary a bit by connection type, but most of the options are the same or silimar.

Ctrl+S

Edit the Explicit Sort Value.

[ / ]

Navigate to the previous/next entry in the directory without returning to the directory listing. This also works in Font Management and Sort Profile field editors.

Name

The name of the entry.

Phone Number (Modem only)

The phone number to dial.

Device Name (Serial and 3-wire only)

The device name to open.

Command (Shell only)

The command to run (usually a shell such as /bin/sh)

Address

IP address or host name

Connection Type

Protocol to use. See [protocols]. When you change the protocol, the port number value will be updated as well.

Flow Control (Modem, Serial, and 3-wire)

The type of flow control to use. RTS/CTS, XON/XOFF, Both, or None

Stop Bits (Modem, Serial, and 3-wire)

The number of stop bits to use.

Data Bits (Modem, Serial, and 3-wire)

The number of data bits to use.

Parity (Modem, Serial, and 3-wire)

The parity setting to use. Options are None, Even, Odd, Mark, and Space.

TCP Port

The TCP port to connect to.

SSH Username (SSH no auth only)

The username to send for the SSH protocol. Some BBSs have everyone log in to SSH with the same username, then log into the BBS with their name. This allows setting the first username.

BBS Username (SSH no auth only)

The username to send when Alt+L is entered.

BBS Password (SSH no auth only)

The password to send on Alt+L.

Username

The user name to send. Used by SSH, RLogin, and GHost. For other protocols, send when Alt+L is pressed.

Password

The password to send.

GHost Program (GHost)

The program name to send to the remote.

System Password

An additional password that can be sent after the first Alt+L using successive Alt+Ls.

Binmode Broken (Telnet and TelnetS)

Telnet binary mode is broken on the remote system, do not enable it when connecting. This option is to work around a bug in CTRL-C checking in older Synchronet versions.

Defer Negotiate (Telnet and TelnetS)

Some systems have a mailer or other program running on the initial connection, and will either disconnect or just ignore telnet negotiations at the start of the session. When this option is enabled, SyncTERM will wait until it receives a telnet command from the remote before starting telnet negotiation.

Screen Mode

Selects the format of the window when connected. A mode specifies the number of columns, the number of rows, the aspect ratio, and the font size. Some modes such as the Commodore, Atari, Prestel, and BBC Micro modes will also change the selected font. If the current font is a mode-specific one (such as C64 or Atari), Changing from that mode to a standard one will change the font to Codepage 437 English. A few of the modes will result in different terminal emulation than the ANSI-BBS described in the CTerm documentation. These are:

C64, C128 (40col), and C128 (80col)

These modes use PETSCII controls and do not support ANSI-BBS.

Atari, Atari XEP80

These modes use ATASCII controls and do not support ANSI-BBS.

Prestel, BBC Micro Mode 7

These modes use Videotex controls. There are subtle differences between them, but the most obvious differnces are the Prestel mode doesn’t scroll and Return will send # instead of a carriage return.

Atari ST 40x25, Atari ST 80x25, and Atari ST 80x25 Mono

These modes use the Atari ST VT52 emulation and do not support ANSI-BBS.

See Current Screen Mode and Custom Screen Mode for additional information.

Terminal Type

Sets the value send by protocols that support sending this as a string. Currently, Telnet, RLogin, SSH, and Shell. When this value is empty (displays as "<Automatic>"), it is chosen based on the screen mode as one of syncterm, PETSCII, ATASCII, Prestel, Beeb7, or AtariST+VT52.

Hide Status Line

Indicates that the "status line" at the bottom of the window when connected should not be displayed. This allows for an extra line of text from the remote to be shown.

Download Path

The location to save downloaded files.

Upload Path

The location to start at when browsing for files to upload.

Log Configuration

This brings up a sub-menu to control a debug log. There are four options in this sub-menu:

Log Filename

If this is not blank, specifies the file to write the log data to. When this is blank, disable logging.

File Transfer Log Level

May be one of None, Alerts, Critical Errors, Errors, Warnings, Notices, Normal, or Debug.

Telnet Command Log Level

Chosen from the same list as above.

Append Log File

If set you Yes, the log file retains old information and will keep growing. If set to No, the log file is emptied for each new connection.

Comm Rate (Modem, Serial, and 3-wire)

Specifies the speed to open the serial port at.

Fake Comm Rate (network connections)

Specifies the character pacing display speed for network connections. This controls the simulated baud rate shown in the status line and the character output pacing.

ANSI Music

There are three options in this sub-menu.

ESC [ | only

With this setting, ANSI music is fully compliant with the standards (ECMA-48, ANSI, etc), but almost no software works with this.

BANSI Style

Supports both the SyncTERM (CSI |) and BananaCom (CSI N) ANSI music styles. Support is still very rare, but slightly more common than the first.

All ANSI Music Enabled

In addition to the previous two, also supports CSI M for ANSI music. This is by far the most common sequence used by software that supports ANSI music. Unfortunately, this prevents the ANSI Delete Line sequence from working correctly.

Address Family

Selected IP address family for network connections.

As per DNS

Uses the first address returned by getaddrinfo()

IPv4 only

Will only connect over an IPv4 address. If none is available, the connection will fail.

IPv6 only

Will only connect over an IPv6 address. If none is available, the connection will fail.

Font

Choses a font (and by implication, a codepage) for the connection. Custom fonts are also listed in this menu.

Hide Popups

Do not show status and progress popups.

RIP

Selects the version for Remote Imaging Protocol ("RIP"). RIP allows graphics and mouse usage, and was used by doors and BBSs starting in the early 90s. The RIP support in SyncTERM is not complete, and may not be compatible with other terminals. RIPv1 is the one most commonly used by old BBS software, and it requires that the Screen Mode be set to an EGA mode. RIPv3 is an updated version that is not backward compatible, but can be used in any mode.

Force LCF Mode

This setting will force the DEC terminal "Last Column Flag" mode to always be enabled. This mode is almost always used in modern terminal emulators, which are almost all VT-102 emulators at least. LCF controls the wrapping behaviour when the cursor is on the last column of a line. The specific rules used are complex and not implemented the same in all terminal emulators even today.

Yellow is Yellow

By default, SyncTERM displays low-intensity yellow as brown. This originated in the IBM CGA monitors, and was carried forward to EGA and even most VGA modes. Some digital monitors that were CGA compatible did not have the brown hack. While the vast majority of software will assume that low-intensity yellow should be brown, this allows strict standard compliance.

SFTP Public Key

For SSH connections, SyncTERM can open another SSH channel and write the public key to .ssh/authorized_keys on the remote, which will enable authentication using the private key on at least OpenSSH and new versions of Synchronet. This option requires SFTP support from the remote side, and may cause connection stability issues if SFTP is not available or does not work correctly.

Edit Palette

This opens a menu allowing you to cusomize the palette for the entry. If the default palette has fewer than 16 colours, you can add additional palette entries. When you select a colour, you can enter the red, green, and blue values separately, and the resulting colour is shown when possible.

If there are fewer than sixteen entries in the list, the list will be repeated to fill all 16 palette slots. This allows for example, the use 16 colour Amiga VT-52 mode.

Managing Sort Profiles

SyncTERM supports named sort profiles that control the order entries appear in the Directory. Press < or > in the directory listing to cycle through profiles. The current profile name is shown in the title bar.

Four default profiles are provided: Name, Last Connected, Most Called, and Date Added. You can manage profiles by pressing Ctrl+S to open the Sort Profiles manager.

In the Sort Profiles manager:

Enter

Edit the sort fields for the selected profile. In the sort field editor, press Insert to add a field, Delete to remove one, and Enter to toggle between normal and reversed order.

F2

Rename the selected profile (max 19 characters, must be unique).

Insert

Create a new profile.

Delete

Delete the selected profile. Deleting the last profile reloads the defaults.

F5 / Ctrl+Insert

Copy a profile to the clipboard.

Shift+Delete

Cut a profile (remove and place on clipboard).

F6 / Shift+Insert

Paste the clipboard at the current position. If the name already exists, you will be prompted for a new unique name.

Sort profiles are stored in the [SortProfiles] section of the SyncTERM INI file.

The Explicit Sort Value exists to manually control the position of entries in the list, and can be accessed using Ctrl+S in the Edit Directory Entry menu.

Viewing the Scrollback

When viewing the scrollback, the following keys are supported:

Move up one line

J

Move up one line

Move down on line

K

Move down one line

Page Up

Move up one screen

H

Move up one screen

Page Down

Move down on screen

L

Move down one screen

Escape

Exit scrollback mode

SyncTERM Settings

The SyncTERM Settings menu has the following options:

Web Lists

Add web lists which SyncTERM will synchronize with an http:// or https:// URI at each program startup. Web lists are added as system lists, which means credentials and paths will come from the Default Connection Settings, and they will be copied into your personal list if you use or modify them. Note that the web client will only do GET requests, and only supports status codes of 200 (OK) and 304 (Not Modified). Specifically, it does not support any redirection.

Default Connection Settings

Set the default values for a new directory entry. See Edit Directory Entry for details on these options.

Current Screen Mode

Changes the current screen mode. For directory entries where the screen mode is "Current", will be used during the connection. This setting is not saved across program restarts. To change the startup screen mode, see SyncTERM SettingsProgram Settings → Startup Screen Mode.

Font Management

Allows setting up custom font files.

Program Settings

Allows changing settings that are preserved across reboots. Refer to the Program Settings section for details.

File Locations

Shows the paths to the various files and directories that SyncTERM will access.

Build Options

Shows which optional components SyncTERM was built to support.

List Encryption

Allows you to encrypt or decrypt the BBS list. Choose the encryption type you want or change the password, etc.

Current Screen Mode

This temporarily sets the screen mode. A screen mode defines the number of rows and columns in the window, which font size to use (8x8, 8x14, or 8x16), and the aspect ratio to scale pixels to. The majority of these modes are based on historical analog hardware modes, so most of them do not use square pixels. The main exceptions are LCD80x25, which is an 80x25 mode that uses square pixels and the 8x16 font, and VGA80x25, which is an 80x25 mode that uses square pixels, the 8x16 font, and performs the VGA column expansion to use 9 pixel wide cells. For further details, see the Text Modes section of the ciolib chapter.

Font Management

The Font Management menu allows you to add and remove fonts. Each font should have a unique name, and at least one file for 8x8, 8x14, or 8x16 fonts. The font format is the one used by "DOS fonts". You can insert and delete items using normal UIFC commands.

Program Settings

Confirm Program Exit

Asks if you are sure you want to quit when pressing ESC or right-clicking in the main SyncTERM screen.

Prompt To Save

If enabled, when SyncTERM is started with a URI that is not in a dialing directory, asks if you want to add the entry to the directory.

Startup Screen Mode

The screen mode that is used when SyncTERM starts.

Video Output Mode

The method of displaying SyncTERM output. The options will vary by OS, compile-time options, and installed libraries. See the -I option to SyncTERM for details on the various modes.

Default Cursor Style

The cursor style used by default. Options are "Default (set by video mode)", "Blinking Underline", "Solid Underline", "Blinking Block", and "Solid Block". The remote system may override this using DECSCUSR.

Audio Output Mode

Allows disabling different output methods. SyncTERM attempts to use them in the order listed in the menu, and the first one to succeed is used going forward.

Scrollback Buffer Lines

The maximum number of lines to keep in the scrollback. Once this number is reached, the oldest lines are removed to make room for new lines.

Modem/Comm Device

The device name for the Modem device. For UNIX-like systems, this will be something like "/dev/ttyd0". For Windows, this will be "COM1" to "COM9". If it’s COM10 or higher, it needs to be specified as "\\.\COM10".

Modem/Comm Rate

Specifies the speed to communicate with the modem at. If set to 0, the speed is not set by SyncTERM, and the default is used.

Modem Init String

The string to send to the modem when the device is first opened to prepare it to be used.

Modem Dial String

A string that is sent immediately before the phone number to cause the modem to dial.

List Path

The path to load the personal dialing directory from.

TERM For Shell

The value that the TERM variable is set to for shell type connections.

Scaling

Select to cycle through the values "Blocky", "Pointy", and "External". Blocky scales each pixel to a rectangle, and Pointy will use 45° angles. External will use hardware scaling if possible. The quality of External scaling varies wildly based on OS, device drivers, and output mode.

Invert Mouse Wheel

Toggles the direction the mouse wheel moves.

Key Derivation Iterations

This specifies the number of iterations to derive a key from a password. The higher this value, the more difficult brute forcing is, but the longer it takes to read the BBS list.

UIFC Colours

Allows changing the colours for the User InterFaCe.

Custom Screen Mode

Allows defining the Rows, Columns, Font Size, and Aspect Ratio of the "Custom" screen mode.

Connected State

When you are connected to a system, there are a number of controls available that are not sent to the remote.

SyncTERM supports OSC 8 hyperlinks. When a BBS sends text with an embedded hyperlink, clicking on the linked text will open the URL in your default browser. When the BBS has mouse capture enabled, use Ctrl+Click instead. Hovering over a hyperlinked region displays the URL in the status bar.

Even without OSC 8 markup, Ctrl+Click on plain-text URLs (starting with http://, https://, ftp://, ftps://, or www.) will detect and open the URL directly. This also works in the scrollback viewer.

On platforms where opening a browser is not possible, the URL is copied to the clipboard instead.

Selecting and Copying Text

Left-drag across the terminal to select a region. The copy is made when the button is released. By default the selection is stream- shaped: it flows from the starting cell to the ending cell across line wraps, trailing spaces are trimmed on each row, and rows are separated by \r\n on Windows or \n on other platforms.

Hold Alt when you press the left mouse button to select a rectangular region instead. The Alt state is sampled once, at the start of the drag, and held for the duration of the drag; pressing or releasing Alt mid-drag does not change the selection shape. Each row of the rectangle is copied as its own line, with trailing spaces trimmed.

If the remote system has mouse capture enabled, use Alt+O to take the mouse back from the remote so that you can select text.

For Curses and ANSI modes, only two controls are avilable, and they are not available in other modes. This is because these two modes don’t have access to keys that could not potentially be sent to the remote. To help avoid conflict with remote systems, the XON (Ctrl+Q) and XOFF (Ctrl+S) codes that are used for software flow control are used.

Ctrl+Q

Disconnects from the current session.

Ctrl+S

Brings up the Online Menu (see below)

For all other modes, the ALT key is used for SyncTERM commands, and the following combinations are supported:

Shift+Insert

Pastes the current PRIMARY selection or clipboard contents to the remote.

Alt+B

View scrollback (See "Viewing The Scrollback")

Alt+C

Capture Control. Allows starting and stopping capturing the session to a file. Useful for stealing ANSIs and debugging emulation issues.

Alt+D

Begins a download from the remote system.

Alt+E

Brings up the Dialing Directory

Alt+F

Allows selecting a different font. In some output modes, the selected font will change text that is already on the screen. In most modes however, only newly displayed text will be in the new font.

Alt+H

Hangup and return to the main menu.

Alt+L

Send auto-login information. For protocols that allow auto- login, only the system password is sent. For all others, the username is sent followed by a carriage return, then the password followed by a CR, then the system password followed by the CR. If any of these are not configured for the current entry, neither them, nor the CR are sent for that item.

Alt+M

Changes the currently supported "ANSI" Music prefix.

Alt+O

Toggles remote mouse support. With the remote capturing mouse events, it can be difficult to select text to copy. Alt+O allows taking the mouse away from the remote.

Alt+U

Upload a file (or files) to the remote. Prompts first for a protocol, then for the file(s) to send. ZMODEM and YMODEM each offer a Batch variant that brings up the multi-file picker so you can queue files from several directories and transfer them in a single session. The other choices (XMODEM-1K, XMODEM-128, ASCII, Raw) transfer a single file.

Alt+X

Disconnect and exit SyncTERM. Does not return to the main menu.

Alt+Z

Brings up the Online Menu (see below)

Alt+

Selects the next fastest character pacing speed. If the fastest speed is currently selected, disables character pacing. If pacing is currently disabled, selects the slowest pacing speed.

Alt+

Selects the next slowest character pacing speed. If the slowest speed is currently selected, disables character pacing. If pacing is currently disabled, selects the fastest pacing speed.

Online Menu

Allows menu-based selection of some of the above options, as well as some less-common operations that don’t have a keyboard shortcut.

Scrollback

Same as Alt+B

Disconnect

Same as Alt+H

Send Login

Same as Alt+L

Upload

Same as Alt+U

Download

Same as Alt+D

Change Output Rate

Allows selecting a specific character pacing to use.

Change Log Level

Temporarily changes the file transfer log level

Capture Control

Same as Alt+C

ANSI Music Control

Same as Alt+M

Font Setup

Same as Alt+F

Toggle Doorway Mode

Turns on or off Doorway mode without the host specifying it. Can be used to recover from broken remote software.

Toggle Remote Mouse

Same as Alt+O

Toggle Operation Overkill ][ Mode

Turns on or off OO][ mode. Can be used to recover from broken remote software.

Exit

Same as Alt+X

Edit Dialing Directory

Same as Alt+E

Techical Details

The following sections delve deeper into technical details, and should not be required for normal use.

Persistant State

SyncTERM will preserve a very small amount of state when exited normally and restore it when the program is restarted. At present, this state is limited to the scaling factor applied to the window. The scaling factor is a floating-point value that indicates the largest value which both the width and height can be multiplied by and still fit inside the current window size. If you manually resize the window only making it wider for example, the additional width will not be saved as the height will control the size the next time SyncTERM is started.

This state is only saved under two specific circumstances. Either the current text mode at exit is the same as the configured Startup Screen Mode, or the Startup Screen Mode is "Current" and the current text mode is "80x25". In all other cases (such as after the Current Screen Mode is changed), it will not be saved. Also, it’s only saved when the program exits normally. In the case of a crash, the setting will not be updated.

Installing Terminfo Entry

When using *nix software through SyncTERM either locally via the shell protocol, or remotely via SSH or as a door on a BBS, it helps immensely if the remote has a terminfo entry for SyncTERM installed. To install the terminfo entry, follow the following steps:

  1. Write the terminfo entry to a file
    syncterm -T > syncterm.terminfo

  2. Compile the entry and install it
    tic -sx syncterm.terminfo
    By default, it is only installed for the current user.

Once the terminfo entries are installed, you can use them by setting the TERM environment variable to syncterm. In Bourne shells, this is usually accomplished with the command export TERM=syncterm.

MBBS GHost

"GHost" in SyncTERM refers to the "Galacticomm Host Program" (called Ghost) that was included in Major BBS and Worldgroup (MBBS/WG) that allowed a Sysop to connect another (DOS-based) PC to the BBS by use of a null modem cable. This was a way for a MBBS/WG Sysop to offer DOS doors, something that wasn’t normally possible.

The functions of the Ghost software itself are beyond the scope of SyncTERM, and you should consult the MBBS/WG Ghost documentation for operation details. However, broadly speaking, it worked like this:

1) MBBS/WG would send a signal down the null modem cable to alert the DOS PC (running Ghost) that it wanted to run a door. 2) Using a simple protocol, MBBS/WG would transmit information required to run the door (username, time remaining, whether ANSI-BBS graphics were supported, etc) to Ghost. 3) Ghost would then launch the door in DOS, using a batch file to call Ghost back once the door exited to wait for the next request.

While few people are connecting DOS-based PC’s to anything by null modem cables anymore, the Ghost protocol (as offered in SyncTERM) is still useful because it’s a way to run DOS doors inside a virtual machine and expose them outside of that virtual machine. The idea being that the VM would configure a serial port as some kind of network passthrough, so when SyncTERM connects, it’s passed through to the VM and then Ghost.

One use case for this is to offer DOS doors in environments where it would normally be difficult or impossible. For example, a UNIX user could run SyncTERM on a remote system in curses mode, where it would then connect to a VM and launch a DOS door via Ghost. This would all be presented to the end UNIX user in a seamless way, so all they would see is the door startup.

The Ghost protocol consists of a single line starting with 'MBBS:', terminated with \r\n, and contains five parameters:

MBBS: PROGRAM PROTOCOL 'USER' TIME GR

You don’t need to worry about sending this since SyncTERM will format it for you based on the SyncTERM configuration options. But it is helpful to understand how various SyncTERM options will translate to the Ghost protocol parameters:

PROGRAM: The name of the DOS door/software to ask the Ghost side to run. Configured in SyncTERM in the 'GHost Program' field of a directory entry, or after the final slash in a ghost:// style URL. For example: ghost://user@203.0.113.64/program

PROTOCOL: Always set to 2. Not configurable in SyncTERM.

USER: Username of the person connecting. Configured in SyncTERM in the 'username' field of a directory entry, or before the '@' in a ghost:// style URL. For example: ghost://user@203.0.113.64/program

TIME: Amount of time the user has remaining. Always set to 999. Not configurable in SyncTERM.

GR: Set to GR (for "GRaphics", meaning ANSI-BBS support) or NG (for "No Graphics"). Always set to GR. Not configurable in SyncTERM.

SyncTERM Wren Scripting Manual

SyncTERM embeds the Wren virtual machine as a per-connection scripting host. Every time SyncTERM connects to a BBS it spins up a fresh Wren VM, loads the user’s scripts, and tears the VM down at disconnect. Scripts hook into keyboard, mouse, inbound bytes, outbound bytes, status text, and a periodic timer; they get a read/write window into terminal and connection state through a small set of foreign classes.

This document is the reference for that scripting layer: how scripts are discovered, how the embedded scripts can be overridden, and the complete add-on object model.

Why Wren

Wren is a small, class-based, dynamically typed scripting language. Three properties make it a good fit for SyncTERM:

  • It compiles to bytecode in-process; no separate toolchain or external interpreter is required.

  • The VM is a few thousand lines of plain C with no external dependencies, vendored under src/syncterm/wren/.

  • The host program controls every binding the script can reach. No filesystem, networking, or process primitives leak in by default.

The full upstream language reference lives at https://wren.io/. This manual covers only the SyncTERM additions.

Quick Start

The minimal "hello world" hook prints a message to the SyncTERM Wren console (Ctrl+`) the first time you press F1 while connected:

import "syncterm" for Hook, Key

Hook.onKey { |k|
  if (k == Key.f1) {
    System.print("hello from wren!")
    return true        // consume the keystroke
  }
  return false         // pass through to SyncTERM
}

Save this as hello.wren in the SyncTERM scripts directory (see Script Loading), connect to any BBS, and press F1. Open the console with Ctrl+` to see the output.

Wren Language Reference

A compact reference for the Wren syntax SyncTERM scripts use. Full upstream documentation at https://wren.io/. Key facts up front: Wren is single-threaded and cooperative (concurrency via fibers, no threads); whitespace-significant (newline terminates statements; no ; token); class-based with single inheritance; dynamically typed.

Comments
// Line comment.
/* Block comment, /* nested */ blocks ok. */
Literals

true false

Booleans.

null

The single null value.

123 0xFF 1.5e3

Numbers. All numeric values are 64-bit floats; 0x for hex; underscores 1_000_000 for digit grouping.

"hello"

String literal. Escapes: \\ \" \% \0 \a \b \e \f \n \r \t \v \xNN \uNNNN \UNNNNNNNN.

"got %(x)"

String interpolation: %(expr) evaluates expr and inserts its toString. A bare % not followed by ( is a lex error — see Strings and %.

[1, 2, 3]

List literal.

{"a": 1, "b": 2}

Map literal.

1..5

Half-open Range (1, 2, 3, 4 — excludes 5).

1...5

Closed Range (1, 2, 3, 4, 5 — includes 5).

Statement Termination

Newlines separate statements. There is no ; token at all. Two statements on one line is a compile error. This trips up developers coming from C / JS / Python regularly.

var x = 1                  // ok
var x = 1 ; var y = 2      // ERROR: Invalid character ';'

Two consequences of the same rule:

  • Ternaries can’t span lines. cond ? a : b must be on one line — the then-branch terminates at a newline before the :.

  • else after a newline-ended if body is a syntax error. if (c) body whose body sits on the same line as the if ends at the newline; an else on the next line is orphaned. Either brace each branch (so else follows }) or put the whole chain on one line.

if (a == "x") foo() else bar()      // single-line, ok
if (a == "x") {                     // braced, ok
  foo()
} else {
  bar()
}
if (a == "x") foo()                 // ERROR: 'else' is orphaned
else bar()
Strings and %

The Wren lexer treats % inside a "…​" string as the start of an interpolation %(expr). A % not followed by ( is a lex error — not a literal %. To put a literal percent sign in a string, escape it as \%:

"100%"          // ERROR: Expect '(' after '%'.
"100\%"         // ok — four bytes: 1, 0, 0, %
"got %(x)"      // ok — interpolation, inserts x.toString

Every literal % needs a leading \. The pair \% is its own escape — \\% does not work, because \\ consumes the slash on its own and leaves a bare %; spell that as \\\%.

Variables
var x = 1                  // declare and initialize
x = 2                      // reassign

Block-scoped (everything between { and }). Shadowing in inner scopes is allowed. Top-level var at module scope is a module-level binding (importable from other modules). Declaration is required before use; there is no auto-vivification.

Operators

Arithmetic: + - * / % (modulo follows the dividend’s sign), unary - and +. Comparison: < ⇐ > >= == !=. Logical: && || ! — short-circuit, return one of their operands (not coerced to Bool). Bitwise: & | ^ << >> ~ — operate on integers (Wren truncates to 32-bit for the bitwise op). Range: .. and .... Conditional: cond ? a : b (one line). Type test: obj is Class.

The .. / ... operators bind tighter than method calls, so write (0...list.count) not 0...list.count when chaining.

Control Flow
if (cond) {
  ...
} else if (cond2) {
  ...
} else {
  ...
}

while (cond) {
  ...
}

for (item in iterable) {
  ...
}

break        // exit innermost loop
continue     // next iteration
return v     // return from method/function

for (x in seq) {} works on anything implementing iterate(prev) and iteratorValue(iter) — Lists, Maps (yields keys), Strings (yields codepoint substrings), Ranges, and your own classes.

Functions and Closures

Wren has no top-level function keyword. Functions are closures created with Fn.new:

var add = Fn.new {|a, b| a + b }
var n   = add.call(3, 4)            // 7

var greet = Fn.new {
  System.print("hi")
}
greet.call()

Block syntax { …​ } after a method name passes a closure as the last argument:

list.map {|x| x * 2 }        // map(_) takes a Fn

Single-line { expr } returns expr implicitly; multi-line bodies return null unless an explicit return is hit. This applies uniformly to Fn.new, Fiber.new, getters, methods, and operator overloads. The inverse is also true: { return expr } on a single line is a compile error — single-line bodies are expression-mode, and return is a statement.

foo() { 42 }                 // ok — implicit return
foo() {                      // multi-line — last expression NOT returned
  var x = 1
  x + 1                      // discarded; foo() returns null
}
foo() {                      // ok — explicit return
  var x = 1
  return x + 1
}
foo() { return 42 }          // ERROR: single-line, return not allowed
Classes

Single inheritance, no abstract classes, no interfaces. All fields are private to the declaring class (see "Field scope" below).

class Animal {
  construct new(name) {
    _name = name
  }

  name { _name }                          // getter
  name=(s) { _name = s }                  // setter

  speak() {                               // method
    System.print("%(_name) makes a sound")
  }

  static kingdom { "Animalia" }           // static getter
  static spawn(n)  { Animal.new(n) }      // static method

  // Operators: + - * / % - (prefix) ! < > <= >= == [_] [_]=(_)
  +(other) { _name + other.name }
}

class Dog is Animal {
  construct new(name, breed) {
    super(name)                           // call super constructor
    _breed = breed
  }

  speak() {                               // override
    System.print("%(name) barks")
  }

  describe() {
    super.speak()                         // call super method
    System.print("It's a %(_breed)")
  }
}
Field scope

Field references (_name, __static) resolve against the class currently being compiled, not the inheritance chain. A subclass' _x is a brand-new slot, not the parent’s _x. Cross class boundaries via getters/setters:

class Widget {
  construct new() { _surface = null }
  surface { _surface }                    // expose to subclasses
}
class Pane is Widget {
  paint() {
    var s = surface                       // ok — uses getter
    var t = _surface                      // BUG: brand-new field, null
  }
}

Symptom of getting it wrong: Null does not implement 'X(,)' errors from subclass methods reading "the parent’s" field.

Naming
  • _name — instance field. Only legal inside a class body; the parser rejects it elsewhere.

  • __name — static field. Same scoping rule.

  • name_ (trailing underscore) — convention for "class-private" methods. Not enforced by the language; a strong project hint.

Foreign methods and classes

foreign declarations bind to host C code:

class Codepage {
  foreign static encodes_(s)              // host-implemented static
  foreign instance_method(arg)            // host-implemented instance
}

foreign class Cell {                      // host owns instance allocation
  foreign ch
  foreign ch=(s)
}

A class needs foreign class only when the host allocates instance data; a plain class with foreign static methods is fine for namespace-style bindings (see Codepage, Hook).

Type checks

obj is Class returns true if Class is in the object’s class chain. Compiles to obj.is(Class). You can override is, but you cannot delegate to the default via super.is(c)is is a reserved keyword and super. requires an identifier after it. Override only if the foreign genuinely implements every method of the claimed class.

Fibers

Fibers are first-class coroutines. Wren is single-threaded and cooperative; concurrency comes from yielding fibers, not threads. There is no await, no Promise, no scheduler — the fiber handle IS the resumption token.

var f = Fiber.new {
  System.print("a")
  Fiber.yield()                  // suspend; control returns to .call() caller
  System.print("b")
}
f.call()                          // prints "a", returns when fiber yields
f.call()                          // prints "b", fiber finishes

// .yield(v) returns v from the .call() that resumed the fiber:
var g = Fiber.new {
  while (true) {
    var x = Fiber.yield(42)       // yield 42, get next call's arg as x
    System.print(x)
  }
}
g.call()                          // returns 42
g.call("hi")                      // prints "hi", returns 42
g.call("yo")                      // prints "yo", returns 42

// .try() catches abort:
var h = Fiber.new { Fiber.abort("boom") }
var err = h.try()                 // err == "boom"; h.error == "boom"

Fiber.yield(v) transfers to the immediate .call() caller — a child fiber yielding inside a hook body returns control to the hook body, NOT up to the dispatcher. This is what makes Fiber.new { …​ }.call() from inside a hook safe.

For host-driven async: a foreign method captures Fiber.current from a slot and returns; the host arranges to call .call(_) on the captured handle later. See Modal Input for the canonical example.

Modules and Imports
import "module"                   // load module, no symbols imported
import "module" for Name          // import a single symbol
import "module" for A, B, C       // multiple
import "module" for Name as Alias // rename on import

Module names are strings; the host resolves them. In SyncTERM, modules are looked up in (1) embedded scripts (compiled in), (2) the user script directory. A user file myhelper.wren in the script dir is importable as import "myhelper".

Common Pitfalls

A checklist of mistakes that catch every new Wren author at least once. Most are consequences of rules above; this section gives them a single place to look up.

  • Semicolons. Wren has no ;. Every separator is a newline.

  • Multi-line bodies don’t auto-return. Use explicit return.

  • Single-line { return x } is a syntax error. Drop return.

  • Ternaries on one line only. Split via if/else or pre-compute.

  • else must follow } on the same line, or chain on one line.

  • Every literal % in a string needs \%. Bare % starts interpolation. See Strings and %.

  • Subclass _field is NOT the parent’s. Use getters/setters.

  • String.count is codepoints; s[i] and s[a...b] are bytes. Mixing them silently truncates UTF-8 strings. For byte iteration use s.bytes.count.

  • String < > >= are not defined. list.sort() on strings aborts; pass an explicit byte-wise comparator.

  • Wren modulo follows the dividend’s sign. -1 % 5 is -1, not 4. For positive-modulo, write ((x % n) + n) % n.

  • foreign class is for instance allocation only. Use plain class with foreign static for namespace bindings.

  • No async / await / scheduler. Use fibers and Fiber.yield.

Wren Standard Library

The built-in classes Wren provides without any host bindings. These are always available, no import needed. Many are mixin-aware via the Sequence protocol, so iteration and transformation methods work on Lists, Maps, Ranges, Strings, and your own classes that implement iterate / iteratorValue.

System

Static-only namespace for I/O and timing.

System.print()

Print a blank line.

System.print(value)

Print value.toString followed by newline.

System.printAll(seq)

Print each item in seq, one per line.

System.write(value)

Print value.toString, no newline.

System.writeAll(seq)

Like printAll without newlines.

System.clock

Wall-clock time in seconds since process start, as Num.

Object

Root of every class hierarchy. Every value responds to:

obj == other

Identity by default; classes can override.

obj != other

Negation of ==.

obj.hash

Hash code, integer.

obj.is(class)

Type check; same as obj is class.

obj.toString

Default returns "instance of <Class>". Override for nicer output.

obj.type

Returns the object’s class.

!obj

Boolean negation. Falsy values are false and null only — every other value (including 0 and "") is truthy.

Class

The class of all classes. Useful properties:

Cls.name

String name of the class.

Cls.supertype

Parent class, or Object for Object itself.

Cls.toString

The class name.

Bool

true and false. Has toString (returns "true" / "false") and the standard ! && || operators (the latter two short- circuit and return one of the operands, not a coerced Bool).

Null

The single value null. !null is true; everything else (null.toString, null == null) does what you’d expect.

Num

All numeric values are 64-bit floats. No separate integer type; the language calls something an "integer" when it has no fractional part. Mathematical method-style helpers:

Num.pi

π.

Num.infinity

+∞.

Num.largest, Num.smallest

Max / min finite double.

Num.maxSafeInteger, Num.minSafeInteger

Range of exact-integer doubles (±2⁵³−1).

n.abs

Absolute value.

n.ceil, n.floor, n.round, n.truncate

Round-modes.

n.sqrt

Square root.

n.sin, n.cos, n.tan

Trig (radians).

n.asin, n.acos, n.atan

Inverse trig.

n.atan(x)

atan2(self, x).

n.log, n.log2, n.exp

Natural log, log base 2, eˣ.

n.pow(p)

nᵖ.

n.min(other), n.max(other)

Pairwise min / max.

n.clamp(lo, hi)

Clamp into [lo, hi].

n.fraction

Fractional part.

n.sign

-1 / 0 / +1.

n.isInteger, n.isInfinity, n.isNan

Predicates.

n.toString

Decimal string.

n & m, n | m, n ^ m, ~n

Bitwise (truncated to 32-bit).

n << k, n >> k

Bit shifts (also 32-bit).

String

Immutable Unicode string, stored as UTF-8. s.count is codepoint count; s[i] and s[a...b] are byte-indexed. This pair of facts is the single most insidious Wren string trap — see Common Pitfalls. For byte work, use s.bytes.

s.count

Codepoint count.

s.isEmpty

True iff count == 0.

s.bytes

A Sequence view yielding each byte as a Num.

s.codePoints

A Sequence view yielding each codepoint as a Num.

s[i]

Codepoint substring starting at byte index i. If i is mid-codepoint, returns the raw byte.

s[a..b], s[a...b]

Byte-ranged substring. Negative indices count from the end.

s + other

Concatenation.

s * n

Repeat n times.

s.indexOf(needle)

Byte index of first match, or -1.

s.indexOf(needle, start)

Byte index of match at-or-after start.

s.contains(needle)

Substring presence.

s.startsWith(prefix)

Prefix check.

s.endsWith(suffix)

Suffix check.

s.replace(old, new)

All non-overlapping occurrences.

s.split(separator)

List of substrings, separator removed.

s.trim(), s.trimStart(), s.trimEnd()

Strip whitespace.

s.trim(chars), s.trimStart(chars), s.trimEnd(chars)

Strip any chars in the supplied string.

s.iterate(prev), s.iteratorValue(iter)

Sequence protocol: yields one codepoint substring per step.

s.toString

The string itself.

String.fromByte(n)

Single-byte string.

String.fromCodePoint(cp)

Encode a codepoint as UTF-8 string.

List

Mutable, ordered, dynamic-length, heterogeneous.

List.new()

Empty list.

List.filled(size, value)

Pre-fill size copies of value.

[a, b, c]

Literal.

list.count

Length.

list.isEmpty

True iff empty.

list[i], list[i] = v

Access / replace; negatives count from the end.

list[a..b], list[a...b]

Slice (returns a new List).

list.add(v)

Append.

list.addAll(seq)

Append every item of seq.

list.insert(i, v)

Insert v at index i.

list.remove(v)

Remove first occurrence; returns true if removed.

list.removeAt(i)

Remove and return element at i.

list.clear()

Empty the list.

list.indexOf(v)

Index of first match, or -1.

list.contains(v)

Membership.

list.sort()

In-place sort with <. Aborts on Strings (no < defined) — pass a comparator.

list.sort {|a, b| a < b }

In-place sort with a custom comparator.

list.swap(i, j)

In-place swap.

list.iterate(prev), list.iteratorValue(iter)

Sequence protocol.

list.toString

"[a, b, c]".

Map

Mutable hash map. Keys may be any hashable value (Num, String, Bool, Null, Range, or Class — not List, Map, or arbitrary instance). Iteration yields keys.

Map.new()

Empty map.

{"a": 1, "b": 2}

Literal.

m.count

Number of pairs.

m.isEmpty

Empty?

m[k], m[k] = v

Access / set. Missing key returns null.

m.containsKey(k)

Distinguishes "missing" from "value is null".

m.remove(k)

Remove and return the old value (or null).

m.clear()

Empty the map.

m.keys, m.values

Sequence views (lazy).

m.iterate(prev), m.iteratorValue(iter)

Sequence protocol; iterating yields keys.

m.toString

"{a: 1, b: 2}".

Range

Created via the .. (half-open) and ... (closed) operators on Nums. Lazy — values are produced on iteration.

r.from

Lower bound.

r.to

Upper bound.

r.min, r.max

Bounds normalised regardless of direction.

r.isInclusive

True for ..., false for ...

r.count

Number of values.

for (i in r) {}

Iterate.

Reverse ranges are valid: 5..0 walks 5, 4, 3, 2, 1.

Sequence (mixin)

The base "iterable" protocol. Sequence itself isn’t usually instantiated; List, Map, Range, String, and your own classes that implement iterate(prev) and iteratorValue(iter) inherit its methods. Most are lazy where it makes sense.

seq.all { fn }

True iff fn(item) is truthy for every item.

seq.any { fn }

True iff any item passes.

seq.contains(value)

Membership via ==.

seq.count

Eager count.

seq.count { fn }

Items where fn(item) is truthy.

seq.each { fn }

Apply fn for side-effects.

seq.isEmpty

True iff count == 0.

seq.join()

Concatenate all items' toString.

seq.join(sep)

…with sep between consecutive items.

seq.map { fn }

Lazy mapped sequence.

seq.where { fn }

Lazy filtered sequence.

seq.skip(n), seq.take(n)

Lazy.

seq.reduce { |a, b| …​ }

Fold from first item.

seq.reduce(seed) { |a, b| }

Fold from seed.

seq.toList

Materialise into a List.

Fiber

First-class coroutines.

Fiber.new { …​ }

Create a paused fiber whose body is the closure.

Fiber.current

The fiber currently running.

f.call(), f.call(value)

Resume f; the value is the return of the most recent Fiber.yield inside f.

f.transfer(), f.transfer(value)

Like call, but does not record this fiber as the resumer; cannot be returned to via Fiber.yield.

f.transferError(err)

Resume f with err raised inside it.

f.try(), f.try(value)

Like call, but if f aborts the abort is caught and f.error is set.

f.isDone

True after the body returns or aborts.

f.error

The abort message if try caught one, else null.

Fiber.yield()

Suspend; control returns to the most recent .call() caller.

Fiber.yield(value)

…passing value as the return of that .call().

Fiber.abort(err)

Raise err in the current fiber. Caller’s .try() catches it.

Fn

A callable closure. Fn.new { …​ } is the only constructor.

Fn.new { …​ }

Build from a closure literal.

f.call()

Invoke with no args.

f.call(a, b, …​)

Invoke with args (up to 16).

f.arity

Declared parameter count.

Script Loading

Script Directory

User scripts live in a per-platform directory. SyncTERM creates the directory on first launch if it doesn’t already exist.

Platform Scripts directory

Linux / *BSD

$XDG_DATA_HOME/syncterm/scripts/
(default ~/.local/share/syncterm/scripts/)

macOS

~/Library/Application Support/SyncTERM/scripts/

Windows

%APPDATA%\SyncTERM\scripts\

Haiku

~/config/settings/SyncTERM/scripts/

The directory has two roles, distinguished by where files live within it:

Path Role

scripts/<name>.wren

Pure library module. Loaded only when something imports it via import "<name>". Has no auto-run side effects. Where you put reusable code that other scripts pull in.

scripts/auto/<event>/<name>.wren

Auto-loaded entry script. Runs at the moment the framework fires <event>; typically registers hooks. Currently the only event is connected, fired once per BBS session, but the layout reserves room for startup / ssh / ui / etc. as new auto-load points are added.

Files anywhere else under scripts/ (e.g. user-organised subdirectories of pure library code) are reached only via import — the framework owns scripts/auto/ and ignores everything else for auto-load purposes.

Module Names

Each script becomes its own Wren module, named after the file basename without the .wren extension and without any directory prefix. myscript.wren becomes module myscript, whether it’s at scripts/myscript.wren or scripts/auto/connected/myscript.wren. Module names are how scripts cross-reference each other:

import "myscript" for SomeClass

Module names are also how the embedded-script override mechanism works (see below).

Embedded Scripts

SyncTERM ships with a small set of scripts compiled into the binary. They are not stored as files; the build system runs wren_embed_gen at compile time, infers each script’s role from its source path (library vs auto-load, and which event for auto-load), and folds the sources into a C string table linked alongside wren_host.c.

The currently embedded scripts are:

Module Role Purpose

syncterm

library

Foundational module: foreign-class declarations and Wren-side helpers (REPL, Key, Color, Codepage, …). Every other script imports from this one. Loads on first import.

console

auto / connected

Wren REPL. Triggered by Ctrl+`.

connected

auto / connected

Alt+L send-login handler. Sends username, password, and sysop password from the directory entry, with the right send-order rules per connection type.

The syncterm module contains only declarations; it has no top-level side effects and is loaded on demand the first time something imports it. The auto-load embeds run at the start of each BBS session.

Overriding Embedded Scripts

A user script whose basename and role match an embedded module overrides the embedded one. If you create ~/.local/share/syncterm/scripts/auto/connected/console.wren, your version replaces the built-in REPL; the embedded version is not loaded. The override runs as the same module name (console), so other scripts that import "console" see your version.

The check is exact match on the bare module name within the same auto-load directory. console.wren at the script root would not override the auto-load console — it would simply be a separate library module also named console (and would only be reached via import, never auto-run). Case matters too: Console.wren is a different module from console.wren.

To opt out of an embedded auto-load script entirely, drop a stub override into the matching directory:

// ~/.local/share/syncterm/scripts/auto/connected/connected.wren
// Disable the default Alt+L handler.

The override is loaded but registers no hook, so Alt+L silently does nothing.

Load Order
  1. scripts/auto/<event>/ is globbed for the firing event.

  2. Each embedded script tagged with the firing event runs, unless its module name appears in the user-script set (in which case the user override runs instead).

  3. Each user script in scripts/auto/<event>/ runs as its own filename-derived module.

import statements (whether from a script’s top-level or inside its body) resolve through one lookup chain:

  1. scripts/<name>.wren — pure library module.

  2. scripts/auto/connected/<name>.wren — catches imports of names that are also auto-load entry scripts (rare, but possible if one auto-load module imports another that hasn’t auto-loaded yet).

  3. The embedded table — built-in fallback by module name.

The foundational syncterm module lazily loads the first time anything imports it; subsequent imports hit Wren’s module cache.

A script’s top-level code runs once per connection, at the moment its event fires. Top-level code typically registers hooks; persistent state should be stored on classes (static fields).

Per-Connection Lifecycle

The VM is created in wren_host_init(bbs) just before SyncTERM’s main loop and freed in wren_host_shutdown() at every exit path.

Every reconnect rebuilds the VM from scratch — module-level static fields, registered hooks, and timer entries do not survive a disconnect. A script that needs cross-session state must persist it manually, either through Cache (see Cache) or by writing configuration into the BBS list.

The owner thread is captured at init; dispatchers called from any other thread (background SFTP and SSH writes invoke conn_send from worker threads) short-circuit to pass-through. Scripts run on the foreground thread only.

Importing the API

The host bindings live in module syncterm. Every script — embedded or user, entry or library — imports the classes it needs:

import "syncterm" for Screen, Input, Conn, Hook, Key

Cache is injected into the syncterm module from C as a module-level Directory object — there is no Wren-callable constructor. Import it like any other binding:

import "syncterm" for Cache

Hook Events

Hooks are registered as Wren callables (block, function, or method reference). Each call site walks its hook list in registration order; the first hook returning true consumes the event and stops dispatch. Returning false, returning a non-Bool, or throwing passes through to the next hook and ultimately to SyncTERM’s default handling.

Every registration returns a HookHandle (or null if the per-event limit is hit) — see HookHandle. Save it if the script needs to remove the hook later or read its metrics; toss it if not.

Hooks must run synchronously

Every hook fire is wrapped in a child fiber via Hook.dispatch_. The dispatcher needs the hook’s return value (a Bool for consume / passthrough, a String for onStatus) before it can decide what to do next; a hook that yields up to the dispatcher would strand it with no value to act on.

If a hook callback yields its own fiber directly (e.g. fires an async op against Fiber.current and immediately calls Fiber.yield()), the dispatch wrapper detects the yield and logs:

hook handler must not yield directly; wrap parking work in
Fiber.new { ... }.call()

The hook is then treated as if it returned a non-Bool — the input passes through to the next hook and to SyncTERM’s default handling. A Fiber.abort from the hook is caught the same way: logged with a stack trace, treated as passthrough.

A hook that needs to wait on async results has two options that keep the hook itself synchronous. The first wraps the work in a child fiber and `.call()`s it from the hook body:

Hook.onKey { |k|
  if (k == Key.f2) {
    Fiber.new { runModalBrowser() }.call()
    return true
  }
  return false
}

.call() runs the child fiber synchronously until it yields (e.g. on the result of Input.nextEvent or an SFTP op). The child’s yield returns to the hook body — the immediate .call() caller — not up to the dispatcher. The hook still returns its Bool; the child resumes later via the framework’s result-queue drain.

The second option is to fire the async op against a Fiber.new {|r| …​ } whose body runs when the result arrives. The hook returns synchronously without ever yielding; the callback fiber is invoked later by the drainer:

Hook.onKey { |k|
  if (k == Key.f2) {
    SFTP.realpath(Fiber.new {|r|
      // handle r …
    }, ".")
    return true
  }
  return false
}
Method Argument Return contract

Hook.onKey { |k| …​ }

k: 16-bit ciolib key code (see Key)

Bool — true to consume the keystroke

Hook.onKey(key, fn)

Filtered: fn(k) only fires when k == key. C-side filter — no Wren entry on misses.

Bool — true to consume

Hook.onInput { |b| …​ }

b: one byte received from the remote

Bool true drops the byte; String replaces it with the string’s bytes (up to 256 — larger replacements log a runtime error and the original byte passes through); anything else passes through.

Hook.onInput(byte, fn)

Filtered: fn(b) only fires when b == byte. C-side filter.

Same return contract as the unfiltered form.

Hook.onMatch(pattern, fn)

fn(m) fires when pattern matches the input stream. See "Streaming regex hooks" below.

Ignored — onMatch is passthrough-only. Use Hook.onInput (per- byte, drop / replace) when you need to remove bytes from the wire.

Hook.onMatchClean(pattern, fn)

Same as onMatch, but bytes are pre-filtered through SyncTERM’s shared ANSI parser so colour codes, cursor moves, and DCS / OSC strings never split a literal pattern. A pattern like "Welcome" matches even when the BBS sends "We" + ESC[1;33m + "lcome".

Ignored — passthrough-only, same contract as onMatch.

Hook.onMouse { |ev| …​ }

ev: 7-element list [event, bstate, modifiers, sx, sy, ex, ey]

Bool — true to consume

Hook.onMouse(event, fn)

Filtered: fn(ev) only fires when ev[0] == event. C-side filter.

Bool — true to consume

Hook.onStatus { |def| …​ }

def: SyncTERM’s default status string

String to replace the status, or non-string to fall through

Hook.every(ms, fn)

ms: milliseconds; fn: callable

(none — return value ignored)

Filtered variants share the same per-event registration array as the unfiltered forms, so dispatch order = registration order regardless of which form was used.

Hook.onInput (and Hook.onMatch) fires on every byte off the wire, before RIPscrip parsing, ZMODEM/OOII detection, or the cterm emulator. Scripts see the raw stream and can drop bytes that would otherwise be consumed by those layers, or expand a single byte into multiple bytes by returning a String — the canonical example is LF→CRLF normalization for hosts that send only LF. Bytes are dispatched in bulk right after conn_recv_upto, so there is no speed-emulation gating — parse_rip already eats RIP escapes in bulk regardless of the emulated bps rate, so gating only the Wren hook would be inconsistent.

Hooks run in registration order; the first one that returns Bool true (drop) or a String (replace) wins, and later hooks don’t see that byte. When a replacement won’t fit in the post-filter buffer, the filter pauses on that input byte; the unprocessed wire-side tail stays parked until the next recv_bytes() call drains something out and frees room for it.

Hook.every fires from the main-loop deadline check just before the sleep call. If the loop stalls long enough that more than one interval has elapsed, the deadline jumps forward to "now" rather than firing repeatedly to catch up.

Hook.onStatus is called from the status-bar composer. Returning a new string replaces SyncTERM’s default status text for the next redraw. Returning anything that isn’t a string passes through.

Streaming regex hooks

Hook.onMatch(pattern, fn) registers a regex against the inbound byte stream. Each input byte is fed to a streaming Pike VM (Russ Cox’s NFA simulation, vendored under re1/); when a match completes, fn is called with a Wren List:

Hook.onMatch("login:") { |m|
  Conn.send("user\r")
  return false
}

Hook.onMatch("user (joe|jane|bob)") { |m|
  // m[0] = "user joe", m[1] = "joe"
  System.print("hi %(m[1])")
  return false
}

m[0] is the matched substring; m[1..] are the user-pattern’s capture groups in registration order. Both onMatch and onMatchClean are passthrough-only — the callback’s return value is ignored, and the matched text always reaches the terminal. The historical "Bool true to drop the matched bytes" contract was misleading: it dropped only the single byte that completed the match (every prior byte already went through cterm), so users never actually got a clean "drop the matched span." Anyone needing wire-level drop should use Hook.onInput, which is byte-granular by design.

Hook.onMatchClean(pattern, fn) is the escape-aware variant — the byte stream feeding the regex VM is pre-filtered through SyncTERM’s shared ANSI parser (ansi_filter in ansi_filter.[ch], the same state machine ripper.c uses to find ANSI envelopes inside RIP). The filter strips ESC, CSI sequences (ESC [ …​ <final>), DCS / OSC / PM / APC strings (ESC P|]|^|_ …​ ESC \), and SOS-style strings (ESC X …​ ESC \). The cleaned bytes reach the regex VM in the same per-byte-stream form as onMatch; what the user sees is what the matcher sees.

Grammar

RE1 implements a deliberately minimal regex dialect. These are all of the supported metacharacters:

Construct Meaning

literal byte

matches itself. Backslash has no special meaning — \ matches a literal backslash. To match an operator metacharacter literally, surround it via a non-capturing group with itself escaped is not possible; instead choose patterns that don’t conflict.

.

any byte (including NUL — the streaming VM does not treat NUL as end-of-input)

(…​)

capturing group

(?:…​)

non-capturing group

|

alternation

* + ?

greedy: zero-or-more, one-or-more, optional

*? +? ??

lazy (non-greedy) variants

Notably not supported (will be parsed as literal characters or syntax errors):

  • backslash escapes — \n, \t, \d, \w, \s, \xNN, …​

  • character classes — [abc], [a-z], [^…​]

  • anchors — ^, $, \b, \B

  • backreferences — \1, \2, …​

  • counted repetition — {n}, {n,m}

  • POSIX classes — , …​

  • inline flags — (?i), …​

Patterns are anchored at the current buffer start. "Match anywhere in the stream" is achieved by the dispatcher: when the VM returns IMPOSSIBLE (no thread can ever complete from the current buffer head), the oldest byte is dropped and the survivors are re-fed. In practice this means a pattern like "hello" matches the substring "hello" wherever it appears in the stream.

This trick requires that the pattern can produce IMPOSSIBLE — i.e., the first byte either advances the NFA or kills every thread. Patterns whose leading construct can match without consuming a byte keep threads alive on every input forever, the buffer fills, and matches start being silently dropped. Hook.onMatch therefore rejects patterns whose leading construct is *, +, or ? at registration time with Fiber.abort:

Allowed (1-byte anchor) Rejected (variable-width leading)

hello

.*hello

.bbs

.+bbs

(a|b)c

a*b

(?:foo|bar)baz

(a|b)*c

If you want a quantifier inside the pattern, anchor it with a fixed prefix: bbs (.) ` rather than `(.) bbs.

Match semantics

Pike VM has a leftmost-first-completing tie-breaking rule: as soon as any thread reaches a Match opcode, the match commits and remaining threads in the current step are discarded. In a streaming context this means open-ended quantifiers fire as soon as the shortest acceptable prefix is seen — a* matches the empty string at the first byte, a+ matches the first 'a' and stops.

For greedy-feeling behavior, terminate variable-length sub-patterns with a literal that follows them: user (.) ` (capture text up to a space) instead of bare `user (.). Newlines must be encoded as literal \n bytes in the Wren string (“\n” in the source code, which Wren resolves to a single 0x0A byte).

Limits
  • Buffer is capped at 4 KB per hook. When a partial match grows past the cap, the oldest half is dropped and the VM restarts — patterns that demand more than 4 KB of context will silently miss.

  • Up to 9 capture groups (RE1’s MAXSUB = 20 minus the whole-match pair).

  • Pattern compile errors (bad syntax, internal asserts) surface as Fiber.abort at the registration site, with the RE1 error text attached. The exception trace points at the offending Hook.onMatch call.

HookHandle

Every successful Hook.on* and Hook.every registration returns a HookHandle. The class has no Wren-callable constructor, so a script can’t fabricate one to remove arbitrary hooks — only its own.

import "syncterm" for Hook

var h = Hook.onKey { |k|
  if (k == 0x4200) {        // F8
    System.print("F8 pressed")
    return true
  }
  return false
}

// ...later...
h.remove()                  // tombstone: no further dispatch
Member Meaning

remove()

Tombstone the hook. Future dispatches skip it. Safe to call from inside the hook’s own callback (the host removes by NULL-ing the callable; in-flight dispatch finishes on the still-allocated resources). Returns true on first call, false thereafter.

callCount

Number of times the host has invoked this hook. Counts every wrenCall, regardless of whether the callback returned truthy or errored.

totalRuntime

Cumulative wall-clock seconds spent inside wrenCall for this hook, measured with xp_timer() before and after each invocation.

minRuntime, maxRuntime

Smallest / largest single invocation time in seconds. Both read back as 0 for a never-fired hook; min is seeded on the first call.

HookHandle.remove() is safe from inside the hook’s own callback. The entry’s fn handle is released immediately (dispatchers skip past the now-NULL slot on their next iteration), and the entry is linked onto a cleanup queue. The host drains that queue once per main-loop iteration — outside any dispatcher — at which point the entry is removed from its dispatch array, regex resources (compiled program, match buffer, PikeVM state) are freed, and the entry struct itself is freed once the script has also dropped the HookHandle (Wren’s GC fires the foreign-class finalizer). Metric getters on a removed handle keep working until the script drops the handle.

The conventional Input.next() / Input.next(ms) / Input.poll() primitives all return promptly; while a script is calling them in a loop the doterm() main loop is blocked, and inbound server bytes queue up in the socket buffer until the script returns.

For modal dialogs that should not block the main loop while the script idles between events, use Input.nextEvent(fiber). The foreign registers fiber to receive the next key or mouse event; the caller yields and resumes with a KeyEvent or MouseEvent. Registering does not claim the screen on its own — set CTerm.suspended = true first to halt the wire pump, and clear it again when you’re done:

import "syncterm" for Input, Key, Screen, CTerm

CTerm.suspended = true
Fiber.new {
  while (true) {
    Input.nextEvent(Fiber.current)
    var ev = Fiber.yield()
    if (ev is KeyEvent && ev.code == Key.escape) break
    Screen.window.print("got: %(ev)\n")
  }
  Screen.restore(saved)
  CTerm.suspended = false
}.call()

The doterm() main loop continues to spin normally; when the next key or mouse event arrives, dispatch_key/dispatch_mouse pushes a result onto the framework’s queue and the next main-loop drain calls back into the fiber with the event as its yielded value. The event bypasses every Hook.onKey / Hook.onMouse callback and SyncTERM’s default handling, exactly as if a hook had returned true.

CTerm.suspended is independent of Input.nextEvent — any script can set it to claim the screen, and remote bytes pile up in the conn buffer behind it. When the TCP receive window fills, the remote sees its send() calls block or EAGAIN. Clearing the flag releases the backpressure and the buffered bytes drain through cterm normally.

Input.nextEvent only registers a single fiber; calling it again while another fiber is already registered throws. The fiber must be one created by Fiber.new {}. The root fiber cannot be resumed from C after it yields; passing it just leaves the fiber in a state where its yield never returns.

A hook callback that yields its own fiber directly is detected and reported by the Hook.dispatch_ wrapper (see "Hooks must run synchronously" above). A hook that wants to drive a modal must either spawn a child fiber:

Hook.onKey { |k|
  if (k == Key.f2) {
    Fiber.new {
      while (true) {
        Input.nextEvent(Fiber.current)
        var ev = Fiber.yield()
        if (ev is KeyEvent && ev.code == Key.escape) break
        // ...
      }
    }.call()
    return true
  }
  return false
}

…or register a callback fiber directly so the hook returns synchronously and the fiber’s body runs when the event arrives:

Hook.onKey { |k|
  if (k == Key.f2) {
    Input.nextEvent(Fiber.new {|ev|
      // handle ev …
    })
    return true
  }
  return false
}

Built-in REPL

Ctrl+` opens an immediate-mode REPL implemented in scripts/console.wren. Compiled-in by default; user override via ~/.local/share/syncterm/scripts/console.wren.

While the console is open the doterm() main loop is suspended. Connection bytes accumulate in the socket buffer but aren’t drained until you exit the console. This is acceptable for a development tool; not appropriate for "watch a slow BBS scroll" use cases.

The REPL evaluates input through REPL.eval(module, src). The input is pre-classified by inspecting its leading non-whitespace token: source that begins with a Wren statement keyword (var, class, import, return, break, continue, if, while, for) is compiled as a statement; everything else is compiled as an expression. Successful expressions print their result quoted with C-style escapes (so "7" the string and 7 the number are visibly distinct); statements print nothing.

Variables and class declarations persist across submissions inside the active module:

> var x = 1 + 2
> x * 10
30
> class Foo { static greet { "hi" } }
> Foo.greet
"hi"
REPL Commands
Command Action

/help, /?

Print commands, editing keys, history navigation, scrollback navigation, and exit keys.

/in <module>

Switch the eval target to <module>. Default is syncterm. Use this to inspect or mutate any other loaded module. /in with no argument prints the current target.

/mods

List every Wren module currently loaded into the VM, alphabetically sorted. Includes core (Wren’s built-in classes), every embedded module (syncterm, console, connected), every user script loaded from the scripts directory, and any module pulled in via import.

/quit, /q

Close the REPL and return to the BBS session.

Modules can plug in their own /<command> entries via WrenConsole.register(name, help, fn):

import "console" for WrenConsole

WrenConsole.register("greet", "say hello [<name>]") { |args|
  if (args == "") {
    System.print("hello!")
  } else {
    System.print("hello, %(args)!")
  }
}

The handler is called with the raw argument string — everything after the command name and its first separating space, or "" if none. Re-registering an existing name overwrites the previous binding. Names can’t contain spaces (the dispatcher splits on the first one). The dispatch wraps the handler in Fiber.new {}.try() so a runtime abort surfaces as a logged error instead of tearing the console out from under itself. /? lists every registered command with its help text alongside the built-ins.

WrenConsole.unregister(name) drops a registration (idempotent — no-op if not registered); WrenConsole.commands returns the sorted list of currently-registered names for tooling.

REPL Key Bindings
Key Action

Left / Right

Move the cursor within the current input line.

Home / End

Jump to start / end of the current input line.

Backspace

Delete the character before the cursor.

Delete

Delete the character at the cursor.

Up / Down (live mode)

Walk command history. If the line currently has typed text, that text becomes a prefix anchor — only history entries starting with it are visited.

PgUp / PgDn

Page through the scrollback log buffer. PgUp from live mode pins the current top of view; PgDn returns to live once you’ve scrolled past the tail.

Up / Down (scrollback mode)

Single-row scroll. Down past the tail rejoins live mode.

Enter (blank or whitespace-only)

Advance to a fresh prompt row without evaluating.

Ctrl+W

Backward kill-word, ending at the cursor (the tail past the cursor is preserved).

Ctrl+L

Clear the screen and the in-memory log. Returns to live mode.

Middle-click

Paste from system clipboard at the cursor. Multi-line text is split on LF and each line submitted as if Enter were pressed.

Alt+drag

Hand off to the existing select-and-copy gesture.

Ctrl+` / Esc / /quit

Close the REPL.

Object Model

The remainder of this document is a reference for every foreign class exposed in the syncterm module.

Screen

Read/write access to the terminal display surface. Screen itself exposes whole-screen and absolute-coordinate operations; per-window operations live under Screen.window.

Member Description

Screen.size

[width, height] of the entire screen.

Screen.save()

Save the entire screen contents. Returns an opaque handle.

Screen.restore(handle)

Restore a previously saved screen.

Screen.readRect(sx, sy, ex, ey)

Read a rectangle of cells (1-based, inclusive). Returns a Cells or null.

Screen.writeRect(sx, sy, ex, ey, cells)

Write a list of Cell objects into a rectangle.

Screen.moveRect(sx, sy, ex, ey, dx, dy)

Copy a rectangle to a new position.

Screen.attr=(a)

Set the active text attribute byte.

Screen.hyperlinkId, Screen.hyperlinkId=(id)

Hyperlink ID attached to subsequent window writes. 0 clears. Allocate IDs through Hyperlinks.add.

Screen.supports, Screen.font, Screen.palette, Screen.cursor, Screen.videoFlags, Screen.color, and Screen.window are getters that return the helper class so the script can call its static members.

Screen.supports

Read-only Boolean capability flags reflecting what the current display backend (SDL, X11, Win32, etc.) can do. Scripts that adapt their output to the backend should branch on these.

Flag True when the backend can…

loadableFonts

Replace the bitmap font in any slot at runtime. False on hardware text-mode backends like curses.

altBlinkFont

Render the alternate-blink font slot.

altBoldFont

Render the alternate-bold font slot.

brightBackground

Render the high-intensity (bright) bit on background colors instead of using it as a blink toggle.

paletteChange

Mutate the active palette at runtime.

pixels

Address individual pixels (pixel-aware backends only — needed for RIP, SIXEL, SkyPix).

customCursor

Render a non-default cursor shape.

fontSelection

Switch fonts via the SyncTERM font-selection UI.

windowTitle

Set the OS window title.

windowName

Set the OS window’s class / icon name (X11).

windowIcon

Set the OS window’s icon image.

extendedPalette

Address palette indices beyond 16.

blockyScaling

Use nearest-neighbour scaling for the pixel-doubled display.

externalScaling

Defer scaling to the OS / window manager.

closeLock

Inhibit the OS window close button while a BBS session is active.

Screen.window

Operations scoped to the active text window. Cursor position, character output, line edits, and clearing all act inside the window’s rectangle. Coordinates are window-relative (1, 1 is the top-left of the window, not the screen).

Member Description

Screen.window.bounds, bounds=(box)

[sx, sy, ex, ey] of the active window (screen-absolute, 1-based, inclusive).

Screen.window.position, position=(coord)

[x, y] cursor position, window-relative.

Screen.window.putChar(c)

Write one character at the cursor.

Screen.window.print(s)

Write a string at the cursor.

Screen.window.clear()

Clear the window with the current attribute.

Screen.window.clearToLineEnd()

Clear from cursor to end of line.

Screen.window.deleteLine(), insertLine(), scroll()

Line editing inside the window.

Screen.font

Per-slot font management. Five slots (0..4) match SyncTERM’s font slot model.

Screen.font[i] reads the font index in slot i. Screen.font[i] = n loads font n into slot i. Use the Font class (see Font) for the named built-in indices.

Screen.palette

24-bit-RGB palette as 0xRRGGBB integers.

Screen.palette[i] and Screen.palette[i]=(c) cover the full palette range.

Screen.palette.mode and Screen.palette.mode=(list) get/set the 16-color legacy-attribute palette as a List of integers.

Note
term.c, cterm.c, and ripper.c all do their own palette manipulations. Concurrent edits from a script and from the terminal code may not compose cleanly.
Screen.cursor — CustomCursor

Cursor geometry: scanline range, blink rate, visibility.

The class supports two usage modes:

  • Static (live state): every static property reads or writes the cursor immediately. Screen.cursor.blink = false hides the blink on the next refresh.

  • Instance (staged edit): CustomCursor.new() (or CustomCursor.current) snapshots the current values. Modify the instance’s properties locally, then apply() to commit atomically.

Properties (both static and instance): startLine, endLine, range, blink, visible.

Pre-defined snapshots:

Snapshot Equivalent legacy code

CustomCursor.normal

Mode-default cursor shape (legacy code 2).

CustomCursor.solid

Solid block cursor (legacy code 1).

CustomCursor.none

Hidden cursor (legacy code 0).

Screen.videoFlags — VideoFlags

Boolean video-output flags, with the same static-vs-instance pattern as CustomCursor.

Property Meaning

altChars

Use the alternate (right-half) character set for graphics characters — the EGA/VGA "9th column" expansion.

noBright

Suppress the bright-foreground bit; bold text renders as plain.

bgBright

Treat the high-intensity bit on the background nibble as bright background instead of blink. The terminal’s tradition was blink; most modern terminals bind it to bright bg.

blinkAltChars

Repurpose the blink attribute bit (bit 7 of the legacy attribute byte) to select the alternate character set instead of triggering blink. When set, "blinking" cells render from the alt-font slot statically; when clear, bit 7 means actual blink.

noBlink

Disable blink rendering entirely.

expand

Read-only. True when the backend is using 9-pixel-wide cells with the right-edge duplication for the box-drawing range. Flipping it under a live screen would mismatch cell-bitmap layout sized at video- mode init for 8-vs-9 pixel cells, so it’s intentionally read-only.

lineGraphicsExpand

Apply the right-edge duplication to box- drawing characters in the 0xC0..0xDF range (the "VGA 9th-column" trick).

Screen.color — Color

Build and inspect 24-bit RGB values. Returned values are raw 0xRRGGBB without the high RGB-mode bit; the bit is added when writing into a Cell field via fgRgb= / bgRgb=.

Method Description

Color.fromRgb(r, g, b)

Pack three 8-bit components into a uint32.

Color.fromAttr(attr)

Decode an attribute byte into [fg, bg] 24-bit RGB.

Color.toLegacyAttr(fg, bg)

Encode two palette indices into one attribute byte.

Input

Unified event reader. Replaces the legacy getch / getMouse two-phase split.

Member Description

Input.next()

Block until the next event; return KeyEvent or MouseEvent.

Input.next(ms)

Wait up to ms milliseconds; return the event or null on timeout.

Input.poll()

Return immediately with the next pending event, or null if nothing is pending.

Input.unget(ev)

Push an event back to the front of the input queue. Routes by event type — KeyEventungetch, MouseEventungetmouse.

Input.mousedrag()

Hand off to SyncTERM’s drag-select gesture.

Input.mouseVisible=(b)

Show or hide the mouse cursor. Setter only — there is no ciolib query for visibility, and tracking it from Wren would drift if other code shows or hides the cursor.

Input.nextEvent(fiber)

Register fiber to receive the next key or mouse event. The caller yields to wait; resumes with KeyEvent or MouseEvent. Throws if another fiber is already registered. See Modal Input.

Input.wake(fiber, value)

Queue value to be delivered to fiber via the same result queue Input.nextEvent and Timer.trigger use. Safe to call from inside a hook body — only enqueues; the resume happens on the next main-loop drain. The canonical use case is a network-driven app where Hook.onInput wants to wake a UI fiber that’s parked on Input.nextEvent so the screen repaints when remote bytes change visible state. Multiple wakes queue up; each becomes one resumption. Do NOT call from inside another foreign method whose caller is about to re-enter wrenCall (same rule as wrenCall itself — see wren.md §7).

KeyEvent

Wraps a 16-bit ciolib key code.

Member Description

KeyEvent.new(code)

Construct from a raw 16-bit code.

code

Raw 16-bit ciolib code.

codepoint

Unicode value of the byte under the current Font.codepage for non-extended keys; null for extended keys (high byte non-zero).

text

UTF-8 form of codepoint, or "" for extended keys.

The CIO_KEY_ABORTED scancode (0x01E0, "Esc by scancode") is normalized to plain Esc (0x001B) inside the constructor, so scripts only ever see one Esc value.

MouseEvent

Mirrors struct mouse_event from ciolib.h.

Member Description

MouseEvent.new(event, modifiers, sx, sy, ex, ey)

Construct from raw fields.

event

Mouse event type code.

modifiers

Keyboard modifier mask.

startX, startY

Click-down coordinates (1-based).

endX, endY

Release coordinates (1-based).

Key

16-bit key codes returned by KeyEvent.code.

Printable ASCII characters (digits, letters, punctuation) come through directly as their ASCII byte value with the high byte zero — e.g. uppercase A is 0x0041 (65) — and don’t need a named constant. The constants below cover non-printable keys, modified keys, function keys, and synthetic markers.

The high byte is the PC scancode (or a synthetic marker > 0x7D when no scancode applies); the low byte is 0 for extended keys. ASCII keys use the low byte for the character and 0 for the high byte.

ASCII keys
Constant Code Description

escape

0x001B

Esc. KeyEvent.new also normalises the synthetic CIO_KEY_ABORTED (0x01E0, "Esc-by-scancode") to this value, so scripts only ever see one Esc.

enter

0x000D

Enter / Return (CR).

backspace

0x0008

Backspace (BS).

tab

0x0009

Tab (HT).

delChar

0x007F

ASCII DEL — distinct from delete (the Del key). Some terminals send DEL on Backspace; cterm’s decBkm flag controls which a script sees.

Cursor and editing keys
Constant Code Description

home

0x4700

Home.

end

0x4F00

End.

up

0x4800

Up arrow.

down

0x5000

Down arrow.

left

0x4B00

Left arrow.

right

0x4D00

Right arrow.

pageUp

0x4900

Page Up.

pageDown

0x5100

Page Down.

insert

0x5200

Insert (Ins).

delete

0x5300

Delete (Del) — the keyboard key, distinct from delChar (the ASCII byte 0x7F).

backTab

0x0F00

Shift+Tab (Back-Tab).

Modified Insert / Delete
Constant Code Description

shiftIns

0x0500

Shift+Insert.

shiftDel

0x0700

Shift+Delete.

ctrlIns

0x0400

Ctrl+Insert.

ctrlDel

0x0600

Ctrl+Delete.

altIns

0xA200

Alt+Insert.

altDel

0xA300

Alt+Delete.

Modified arrow keys and End
Constant Code Description

shiftUp

0x3800

Shift+Up.

ctrlUp

0x8D00

Ctrl+Up.

shiftDown

0x3200

Shift+Down.

ctrlDown

0x9100

Ctrl+Down.

shiftLeft

0x3400

Shift+Left.

ctrlLeft

0x7300

Ctrl+Left.

shiftRight

0x3600

Shift+Right.

ctrlRight

0x7400

Ctrl+Right.

shiftEnd

0x3100

Shift+End.

ctrlEnd

0x7500

Ctrl+End.

Function keys

f1 through f12 and the modified variants shiftF1..shiftF12, ctrlF1..ctrlF12, altF1..altF12. Code values are contiguous within each group, but the absolute values are scancode- derived and aren’t worth memorising. Compare against the named constant.

Key Plain Shift Ctrl Alt

F1

0x3B00

0x5400

0x5E00

0x6800

F2

0x3C00

0x5500

0x5F00

0x6900

F3

0x3D00

0x5600

0x6000

0x6A00

F4

0x3E00

0x5700

0x6100

0x6B00

F5

0x3F00

0x5800

0x6200

0x6C00

F6

0x4000

0x5900

0x6300

0x6D00

F7

0x4100

0x5A00

0x6400

0x6E00

F8

0x4200

0x5B00

0x6500

0x6F00

F9

0x4300

0x5C00

0x6600

0x7000

F10

0x4400

0x5D00

0x6700

0x7100

F11

0x8500

0x8700

0x8900

0x8B00

F12

0x8600

0x8800

0x8A00

0x8C00

Synthetic markers

These are SyncTERM-specific codes injected into the key queue rather than scancodes from a real key.

Constant Code Description

mouse

0x7DE0

A mouse event is next in the queue. When KeyEvent.code == Key.mouse, call Input.next() again to read the MouseEvent itself. The Schneider/Amstrad PC1512 "F-14" key encoded as right-mouse — SyncTERM reuses that codepoint.

quit

0x7EE0

The user requested quit (window close button, OS close signal). Modeled on the PC1512 "F-15" codepoint.

wrenConsole

0x29E0

Ctrl+` — opens the Wren console. High byte is the `-key scancode (0x29).

Clipboard
Member Description

Clipboard.text

Read the system clipboard as a string.

Clipboard.text=(s)

Write a string to the system clipboard.

Codepage

Enum-style class identifying a character-set encoding. Returned by Font.codepage and Font.codepageOf(i) to tell scripts which encoding the bytes in a screen cell are in.

A _b suffix on a codepage name means "broken vertical" — the font variant draws box-drawing pipe characters with one-pixel gaps between rows, matching the historical rendering of certain hardware terminals. The base codepage and its _b variant cover the same character set; only the glyph shapes differ.

Constant Index Description

cp437

0

IBM PC CP437 — original DOS character set, English plus box-drawing. The default for traditional ANSI BBSes.

cp1251

1

Windows-1251 — Cyrillic (Russian, Bulgarian, Ukrainian, etc.) in a "swiss" font variant.

cp1251_b

2

Windows-1251 with the broken-vertical glyph variant.

koi8r

3

KOI8-R — Russian Cyrillic. Common on 1990s Russian Linux/Unix systems.

iso8859_2

4

ISO-8859-2 (Latin-2) — Central European (Polish, Czech, Hungarian, …).

iso8859_4

5

ISO-8859-4 (Latin-4) — Northern European / older Baltic. 9-bit-mapped wide-glyph variant.

cp866m

6

CP866 — Russian DOS, "(c)" modified variant.

iso8859_9

7

ISO-8859-9 (Latin-5) — Turkish.

iso8859_8

8

ISO-8859-8 — Hebrew.

koi8u

9

KOI8-U — Ukrainian Cyrillic.

iso8859_15

10

ISO-8859-15 (Latin-9) — Western European with the Euro sign.

iso8859_5

11

ISO-8859-5 — Latin/Cyrillic.

cp850

12

CP850 — Multilingual Latin-1 (DOS Western European).

cp850_b

13

CP850 with the broken-vertical glyph variant.

cp865

14

CP865 — Nordic DOS (Danish, Norwegian).

cp865_b

15

CP865 with the broken-vertical glyph variant.

iso8859_7

16

ISO-8859-7 — Greek.

iso8859_1

17

ISO-8859-1 (Latin-1) — Western European, classic Unix default.

cp866m2

18

CP866 — Russian DOS, second modified variant.

cp866u

19

CP866-U — Ukrainian variant.

cp1131

20

CP1131 — Belarusian DOS.

armscii8

21

ARMSCII-8 — Armenian.

haik8

22

HAIK-8 — older Armenian set; used with the ARMSCII-8 screen map.

atascii

23

ATASCII — Atari 8-bit native character set.

petsciiUpper

24

PETSCII (uppercase/graphics mode) — Commodore 64/128 default.

petsciiLower

25

PETSCII (lowercase/shifted mode) — Commodore 64/128 after a Shift+Commodore key.

prestel

26

UK Prestel / Viewdata teletext — block mosaic graphics, double-height, conceal.

prestelSep

27

Prestel separated mosaics — block mosaics with gaps between cells.

atariSt

28

Atari ST GEM character set.

Cell

Wraps a struct vmem_cell: one screen cell’s character, attribute, and color state. Use cases: read a rectangle through Screen.readRect, build a list manually, then write it back through Screen.writeRect.

Member Description

Cell.new()

Construct an empty cell.

ch, ch=(s)

Character as a Unicode string (one codepoint, round-tripped through CP437).

chByte, chByte=(n)

Character as a raw 8-bit byte.

font, font=(n)

Font slot.

legacyAttr, legacyAttr=(n)

Legacy attribute byte (foreground in low nibble, background in high nibble, plus bright/blink bits).

bright, bright=(b)

Bright bit.

blink, blink=(b)

Blink bit.

fgPalette, bgPalette

Foreground / background palette indices.

fgRgb, bgRgb

Foreground / background as 24-bit RGB. Setters preserve bits 24..30 of the existing field, so toggling between palette and RGB modes does not lose unrelated state.

hyperlinkId, hyperlinkId=(n)

Hyperlink ID attached to this cell.

Cells

Immutable foreign list returned by Screen.readRect. Indexable, iterable, has .count.

Font

A "font" in SyncTERM is one of 257 indexed slots, each a complete glyph table for a particular character set or display style. Slots 0–45 ship with built-in fonts, covering the codepages enumerated by Codepage plus several Amiga and 8-bit display styles. Slots 46+ are reserved for user-loaded fonts (uploaded via cterm escape sequences or RIP), but Font.count reports however many slots are actually populated at runtime.

The Font class itself only exposes named constants for the most useful slots; every slot is reachable numerically through Screen.font[i] regardless. The full built-in table follows.

Named constant Index Slot description

Font.cp437English

0

Codepage 437 English (default).

1

Codepage 1251 Cyrillic, "swiss" variant.

2

Russian KOI8-R.

3

ISO-8859-2 Central European.

4

ISO-8859-4 Baltic wide (VGA 9-bit mapped).

5

Codepage 866 (c) Russian.

6

ISO-8859-9 Turkish.

7

HAIK-8 (used with the ARMSCII-8 screen map).

8

ISO-8859-8 Hebrew.

9

Ukrainian KOI8-U.

10

ISO-8859-15 West European, "thin" variant.

11

ISO-8859-4 Baltic (VGA 9-bit mapped).

12

Russian KOI8-R, alternate (b) variant.

13

ISO-8859-4 Baltic wide.

14

ISO-8859-5 Cyrillic.

15

ARMSCII-8 (Armenian).

16

ISO-8859-15 West European.

17

Codepage 850 Multilingual Latin I, "thin".

18

Codepage 850 Multilingual Latin I.

19

Codepage 865 Norwegian, "thin".

20

Codepage 1251 Cyrillic.

21

ISO-8859-7 Greek.

22

Russian KOI8-R, alternate (c) variant.

23

ISO-8859-4 Baltic.

24

ISO-8859-1 West European.

25

Codepage 866 Russian.

26

Codepage 437 English, "thin" variant.

27

Codepage 866 (b) Russian.

28

Codepage 865 Norwegian.

29

Ukrainian CP866-U.

30

ISO-8859-1 West European, "thin".

31

Codepage 1131 Belarusian, "swiss".

Font.commodore64Upper

32

Commodore 64 (uppercase / graphics).

Font.commodore64Lower

33

Commodore 64 (lowercase / shifted).

Font.commodore128Upper

34

Commodore 128 (uppercase / graphics).

Font.commodore128Lower

35

Commodore 128 (lowercase / shifted).

Font.atari

36

Atari 8-bit.

Font.potNoodle

37

P0T NOoDLE — Amiga BBS classic.

Font.mosOul

38

mO’sOul — Amiga BBS classic.

Font.microKnightPlus

39

MicroKnight Plus — Amiga.

Font.topazPlus

40

Topaz Plus — Amiga.

Font.microKnight

41

MicroKnight — Amiga.

Font.topaz

42

Topaz — Amiga (the original Workbench font).

Font.prestel

43

Prestel — UK Viewdata mosaic.

Font.atariSt

44

Atari ST GEM.

Font.ripterm

45

RIPterm — RIP graphics terminal font.

The "thin" variants are the same character set rendered with thinner strokes, matching late-1980s VGA BIOS fonts. A "swiss" variant uses the Helvetica-style sans rendering common on certain Amiga and DOS displays. An "(a)" / "(b)" / "(c)" annotation marks distinct historical font releases that use the same codepage encoding.

Method Description

Font.name(i)

Human-readable font name for slot i, e.g. "Codepage 437 English". Returns the empty string for empty slots.

Font.count

Total number of populated font slots. Iteration bound — 0…​Font.count walks every available font.

Font.available(i)

true if slot i has actually been loaded (the user can configure SyncTERM to omit some built-ins).

Font.codepage

The Codepage value for slot 0 (whatever codepage the script’s strings should be transcoded into for display).

Font.codepageOf(i)

The codepage value for slot i. Several slots can share a codepage (the Amiga slots 37–42 are all iso8859_1, for example).

Map-like read interface over the active hyperlink table.

Member Description

Hyperlinks[id]

URI string for id, or null.

Hyperlinks.containsKey(id)

true if id is present.

Hyperlinks.add(uri, idParam)

Allocate a new hyperlink ID for uri with optional id parameter string. Returns the new numeric ID.

Hyperlinks.params(id)

Parameter string for id, or null.

To attach hyperlinks to text, allocate an ID and assign it to Screen.hyperlinkId before writing through Screen.window.print.

Console

Read-only access to the always-on print/error log buffer. The buffer has 1024 entries of up to 8 KB each; older entries are evicted in FIFO order. Sequence numbers are monotonic across the program’s lifetime, including across eviction, so an incremental "show only entries newer than X" reader works correctly even after the ring has wrapped.

Member Description

Console.count

Number of valid entries currently in the ring.

Console.total

Monotonic count of entries ever written. Survives clear().

Console[seq]

Entry with monotonic sequence seq, as a tuple [seq, ts, source, text]. source is one of the LogSource values. Out-of-range returns null.

Console.clear()

Drop all entries. total keeps incrementing.

Console.iterate, iteratorValue

Wren iteration protocol — works in for (e in Console) { …​ }.

LogSource

Enum values for the source field on Console entries.

  • LogSource.print (0) — script System.print.

  • LogSource.compileError (1) — Wren compile diagnostic.

  • LogSource.runtimeError (2) — Wren runtime error message.

  • LogSource.stackFrame (3) — one frame of a runtime-error stack trace.

REPL

Primitive for the immediate-mode REPL. Most scripts will not use this directly; the embedded console script wraps it.

Method Description

REPL.eval(src)

Compile and run src in module syncterm. Returns null for statements; [value] (a one-element list) for expressions.

REPL.eval(module, src)

As above, in module.

REPL.hasModule(name)

true if name has been loaded.

REPL.modules

List of every module name currently loaded in the VM (including core). Order is implementation-defined; sort if you need stable output.

The [value] wrapper for expressions distinguishes "this was a statement" from "this was an expression whose value is `null`".

Runtime errors propagate normally. Wrap the call in Fiber.new {}.try() to catch them.

Conn

Connection-level send and receive.

Member Description

Conn.send(s)

Send bytes through the full path: telnet IAC escaping, then on-wire.

Conn.sendRaw(s)

Send bytes raw, bypassing IAC escaping.

Conn.close()

Close the connection.

Conn.connected

Bool — is the link up?

Conn.type

ConnType value.

Conn.pending

Bytes available to read right now (in the inbuf ring).

Conn.queued

Bytes queued to write that haven’t gone out yet.

Conn.peek(n)

Look at up to n bytes from the inbuf without consuming. Clamped to the actual ring size.

Conn.recv(n)

Read up to n bytes from the inbuf and consume them.

CTerm

Read-only window into cterm state — what term.c and ripper.c read. Useful from Hook.onInput and Hook.onStatus for emulation-aware logic.

Group Members

Cursor (1-based, on the terminal)

x, y

Origin (1-based, on the host screen)

originX, originY

Geometry

width, height, topMargin, bottomMargin, leftMargin, rightMargin

Color state

attr, fgColor, bgColor, hasPaletteOverride, paletteOverride

Fonts

fontSlot, altFonts

Scrollback

scrollbackLines, scrollbackWidth, scrollbackPos, scrollbackStart

Mode flags

emulation (Emulation), doorwayMode, music, started, skypix, logMode (LogMode), logPaused, statusDisplay (StatusDisplay)

Bitfield snapshots

extAttr (ExtAttr), lastColumnFlag (LastColumnFlag)

Action Description

CTerm.write(s)

Send s to cterm_write — bypass the wire, render directly.

CTerm.suspended, CTerm.suspended = b

Read or write the wire-pump suspend flag. When set true, the main loop stops draining bytes from the conn buffer. Bytes pile up in the conn buffer; the TCP receive window eventually fills and the remote sees its send() calls block or EAGAIN. Use this to claim the screen for a modal dialog, transfer overlay, etc., without remote output painting underneath. Clear it again when you’re done. Registering a fiber via Input.nextEvent does not suspend on its own — scripts that want modal behavior must set this explicitly. When speed emulation is active and the flag transitions from true back to false, the byte pump is credited with all the bytes that would have processed at the emulated rate during the suspended interval; those bytes drain past the speed gate as fast as the pump can run, so the visible output catches up to where it would have been if the suspend had never happened.

ExtAttr

Snapshot of cterm’s extended-attribute bitfield. All members are read-only Booleans reflecting the corresponding terminal mode.

Member Description

autoWrap

DECAWM — characters past the right margin wrap to the next line.

originMode

DECOM — cursor coordinates are relative to the active scroll region rather than the screen.

sxScroll

SIXEL scroll: when a SIXEL image extends past the bottom of the screen, scroll the screen up to make room. When clear, the image is clipped at the bottom row.

decLrmm

DECLRMM — left/right margin mode enables horizontal scroll regions.

bracketPaste

DEC mode 2004 — pasted text is bracketed by ESC[200~ / ESC[201~ markers.

decBkm

DECBKM — Backarrow key sends BS (0x08) instead of DEL (0x7F).

prestelMosaic

Prestel mosaic graphics character set is active.

prestelDoubleHeight

Current row is rendered double-height.

prestelConceal

Concealed (hidden) text mode.

prestelSeparated

Separated mosaics — gaps between mosaic blocks.

prestelHold

Hold-graphics mode — control codes render as the most recently emitted mosaic glyph rather than blanking.

alternateKeypad

DECKPAM — numeric keypad sends application keypad codes instead of digits.

LastColumnFlag

Snapshot of cterm’s last-column-flag state. Used by emulations that distinguish "cursor at the right margin" from "cursor wrapped to next line" (the so-called phantom-column).

Member Description

set

Bool — the flag is currently set: the cursor sits at the right margin awaiting the next character to wrap it. Cleared by any cursor-positioning sequence.

enabled

Bool — last-column-flag tracking is enabled at all. Some emulations disable it.

forced

Bool — tracking is forced on regardless of the underlying mode (set by SyncTERM’s emulation-bridge code or the BBS list’s forceLcf flag).

LogMode

Referenced by CTerm.logMode.

Constant Code Description

none

0

No log file is being written.

ascii

1

Log decoded ASCII — escape sequences are interpreted, control codes are filtered down to readable text. Good for human review.

raw

2

Log the raw byte stream as received from the remote, including all escape sequences. Good for later replay.

StatusDisplay

Referenced by CTerm.statusDisplay. VT320 DECSSDT (Select Status Display Type) values: which status line, if any, the bottom row is acting as.

Constant Code Description

none

0

No status line — the bottom row is part of the main display area.

indicator

1

Indicator status line — terminal-managed diagnostic line (e.g. "INSERT", connection state). The host can’t write to it.

host

2

Host-writable status line — the bottom row is a separate addressable region the BBS can direct writes to via DECSASD.

BBS

Read-only window into the active struct bbslist entry. Useful from onConnect-style top-level code that wants to behave differently per BBS.

Most fields are direct getters that return the underlying value. Fields that index into enums are surfaced both as the integer (for direct comparison with the enum class) and through the typed enum classes documented below.

Group Fields

Identity

name, addr, port, connType (ConnType), comment, type (BBSListType), id

Network

addressFamily (AddressFamily)

Auth

user, password, syspass, sftpPublicKey, sshFingerprint, sshFingerprintLen

Display

termName, screenMode (ScreenMode), bpsRate, font, noStatus, hidePopups, yellowIsYellow, forceLcf, palette, paletteSize

Modem / serial

stopBits, dataBits, parity (Parity), flowControl (FlowControl)

Telnet

telnetNoBinary, deferTelnetNegotiation, ghostProgram

RIP / music

rip (RipVersion), music (MusicMode)

File transfer

dlDir, ulDir, xferLogLevel (LogLevel), telnetLogLevel (LogLevel)

Logs

logFile, appendLogFile

Statistics

added, connected, fastConnected, calls, sortOrder

ConnType
Constant Code Description

unknown

0

Unset / not yet decided.

rlogin

1

RLogin (TCP/513) — classic BBS auth-on-connect protocol.

rloginReversed

2

RLogin with the handshake byte order reversed (some servers).

telnet

3

Telnet (TCP/23) — IAC negotiation in NVT mode.

raw

4

Raw TCP — no protocol layer; bytes pass straight through.

ssh

5

SSH-2 — encrypted with password / key authentication.

sshNoAuth

6

SSH-2 with the "none" authentication method (anonymous).

modem

7

Phone modem dial-out via AT commands.

serial

8

Direct serial port.

serialNoRts

9

Serial without RTS/CTS hardware flow control.

shell

10

Local subprocess (shell command).

mbbsGhost

11

MajorBBS / Worldgroup "GHost" client interface.

telnets

12

Telnet over TLS (TCP/992).

Emulation
Constant Code Description

ansiBbs

0

ANSI-BBS — ANSI X3.64 / VT-style escape sequences with PC-style color attributes. The default.

petascii

1

Commodore 64 / 128 PETSCII control codes.

atascii

2

Atari 8-bit ATASCII control codes.

prestel

3

UK Prestel / Viewdata teletext — block mosaic graphics, double-height, conceal.

beeb

4

BBC Micro Mode 7 teletext.

atariVt52

5

Atari ST GEM VT-52 emulation.

BBSListType
Constant Code Description

user

0

User-edited entry from the user’s BBS list.

system

1

System-shipped entry from the SyncTERM-bundled list. Read-only at the UI level.

ScreenMode

Standard modes encode their dimensions in the name (e.g. c80x25 = 80 columns × 25 rows). Codes are stable enum values.

Constant Code Description

current

0

"Don’t change" — keep the current mode.

c80x25

1

80×25 (CGA / VGA standard).

lcd80x25

2

80×25 with LCD-style cell aspect (SyncTERM-specific).

c80x28

3

80×28 (extended VGA).

c80x30

4

80×30 (extended VGA).

c80x43

5

80×43 (EGA-extended).

c80x50

6

80×50 (VGA 8-pixel-tall cell).

c80x60

7

80×60 (extended VGA).

c132x37

8

132×37 (Super VGA).

c132x52

9

132×52 (Super VGA).

c132x25

10

132×25.

c132x28

11

132×28.

c132x30

12

132×30.

c132x34

13

132×34.

c132x43

14

132×43.

c132x50

15

132×50.

c132x60

16

132×60.

c64

17

Commodore 64 (40×25 PETSCII).

c128_40

18

Commodore 128 (40-column mode).

c128_80

19

Commodore 128 (80-column mode).

atari

20

Atari 8-bit native (40×24 ATASCII).

atariXep80

21

Atari 8-bit with XEP80 80-column expansion box.

custom

22

Custom dimensions configured per-BBS.

ega80x25

23

EGA 80×25 with hardware-accurate pixel aspect (8×14 cell).

vga80x25

24

VGA 80×25 with hardware-accurate pixel aspect (8×16 cell).

prestel

25

Prestel teletext (40×24).

beeb

26

BBC Micro Mode 7 (40×25 teletext).

atariSt40x25

27

Atari ST 40×25 (low-res color).

atariSt80x25

28

Atari ST 80×25 (medium-res color).

atariSt80x25Mono

29

Atari ST 80×25 monochrome (high-res).

AddressFamily
Constant Code Description

unspec

0

Don’t care — let the resolver pick whichever family the host has.

inet

1

IPv4 only.

inet6

2

IPv6 only.

MusicMode

Controls how SyncTERM interprets ANSI music sequences (CSI …​ character).

Constant Code Description

syncterm

0

SyncTERM’s strict mode — only the CSI MNML sequence triggers music; less-anchored sequences are passed through as text. The default.

bansi

1

BANSI-style — older BBS music parsing rules.

enabled

2

Most permissive — all forms accepted.

RipVersion
Constant Code Description

none

0

RIP support disabled for this connection.

v1

1

RIPscrip v1.54 — the historic dial-up version.

v3

2

SyncTERM’s "idealized" RIP v3 — bug-fixed and extended; not bug-compatible with v1.54.

Parity
Constant Code Description

none

0

No parity bit (8N1 framing).

even

1

Even parity bit.

odd

2

Odd parity bit.

FlowControl

Read-only foreign class — instance returned from BBS.flowControl.

Member Description

rtsCts

Bool — RTS/CTS hardware flow control enabled.

xonOff

Bool — XON/XOFF software flow control enabled.

LogLevel

Standard syslog severity values (RFC 5424). emergency is the most severe; debug is the least.

Constant Code Description

emergency

0

System is unusable.

alert

1

Action must be taken immediately.

critical

2

Critical condition.

error

3

Error condition.

warning

4

Warning condition.

notice

5

Normal but significant.

info

6

Informational.

debug

7

Debug-level diagnostic.

Cache

Module-level Directory injected from C, pointing at SyncTERM’s per-user cache directory (SYNCTERM_PATH_CACHE). Use this for script-private state that should survive restarts.

Cache.list returns a Map keyed by entry name; values are File objects for regular files and Directory objects for subdirectories. That’s the read-side handle factory — a script reads existing cache content by indexing into Cache.list, and creates new content with Cache.create(name):

import "syncterm" for Cache

// Read an existing cache file.  Indexing the list returns the
// File object directly, no separate "open by name" step.
if (Cache.contains("greetings.txt")) {
  var f = Cache.list["greetings.txt"]
  f.open()
  System.print(f.read())
  f.close()
} else {
  // First run: create + write.  create() returns null if the file
  // already exists or the OS rejects the create.
  var f = Cache.create("greetings.txt")
  if (f != null) {
    f.open()
    f.write("Hello, world!")
    f.close()
  }
}

// Subdirectories appear in the same map.  Walk into them by chaining
// `.list` lookups:
//   Cache.list["RIP"]                         → Directory for /RIP
//   Cache.list["RIP"].list["icons.dat"]       → File inside /RIP

There is no Wren-callable factory for DirectoryCache and Cache.list[someSubdir] are the only paths to one.

Directory

Opaque handle pointing at a directory on disk.

Method Description

Directory.contains(name)

true if a file named name exists in the directory.

Directory.list

A Map keyed by entry name; values are File objects for regular files and Directory objects for subdirectories. Symlinks, device nodes, FIFOs, etc. are silently skipped. Names are filtered by the filename policy, so hidden / dotfiles / Windows-reserved names don’t appear. Each read enumerates the directory afresh; cache the value if you’ll touch it more than once. Use this to obtain handles to existing files, or to walk a tree.

Directory.create(name)

Atomically create a new zero-byte file named name in the directory and return a File object for it. Uses C11 exclusive- create mode (fopen(path, "wbx")) so a concurrent creator doesn’t silently truncate an existing file. Returns null if the file already exists, the name violates the filename policy, the path is too long, or the OS rejects the create for any other reason. The file is closed on return — call File.open() before reading or writing.

Directory.createDir(name)

Atomically create a new subdirectory named name and return a Directory object for it. Uses mkdir directly, which fails atomically if the path already exists — same race-free semantics as create() for files. Returns null on the same error conditions: existing entry, invalid name, path too long, or OS rejection (no permission, parent missing, …). Use list[name] to obtain a handle to a subdirectory that already exists.

Directory.delete(name)

Remove the entry named name from the directory. Handles regular files and empty subdirectories; refuses anything else (symlinks, device nodes, FIFOs, non-empty directories). Returns true on successful removal, false on any failure (no such entry, wrong type, non-empty directory, no permission, invalid name, …). On success, every live File / Directory handle whose path is at-or-below the removed entry is marked dead and pulled from the live-handle registry; a subsequent operation on any such handle aborts the calling fiber with a "handle is dead" error.

Handle staleness

File and Directory foreign objects are filesystem snapshots — the C side caches the absolute path at the moment the handle was issued (via Directory.list, Directory.create, Directory.createDir, or Cache). A subsequent Directory.delete invalidates handles to the removed entry by walking a live-handle registry and setting a dead flag on each match.

Two layers of protection keep operations from acting on a stale path:

  1. Active invalidation. Directory.delete walks every live handle and marks dead anything whose path is the removed entry or a descendant of it. Subsequent ops on a dead handle throw.

  2. Per-call existence check. Every File / Directory method re-checks fexist(path) / isdir(path) before doing anything. If the path is missing — because something outside the script (another script, another process, the user) deleted it — the handle is marked dead, removed from the registry, and the operation aborts the calling fiber. This catches deletions that bypassed Directory.delete.

The active layer is fast (one boolean check); the existence layer costs a stat() per operation but covers the externally-modified case. Together they ensure no File / Directory operation ever silently acts on the wrong path.

Open files are exempt

A File handle whose underlying file is currently open (between File.open() and File.close()) is not marked dead by the invalidation walk and bypasses the per-call fexist() check. This matches platform reality:

  • On Unix, an open file descriptor remains valid after the path is unlinked; reads and writes continue to work, and the inode is freed only when the last reference closes.

  • On Windows, deleting a file that’s currently open fails at the OS layer, so the situation never arises.

While the file is open, operations against the fd hit OS-level errors if the underlying file is genuinely gone (very rare on Unix, impossible on Windows). File.close() re-runs the existence check after fclose; if the path is gone at that point, the handle is marked dead and unregistered, and the next operation throws.

Scripts should drop stale handles when they’re done with them; the handles aren’t hazardous (operations throw rather than corrupt state) but holding them prevents the underlying foreign data from being GC’d.

Filename Policy

Filenames passed to any Directory or File method must satisfy:

  • Length 1..64 characters.

  • Characters from [A-Za-z0-9._-] only.

  • Must not begin with . or -.

  • Must not end with ..

  • Must not contain ...

  • Must not match a Windows reserved device name (CON, PRN, AUX, NUL, COM1..COM9, LPT1..LPT9), case-insensitively and with or without an extension.

Method calls violating the policy fail (return null / no-op); methods on the resulting File are no-ops if construction failed.

File
Method Description

File.open()

Open the file (creating if necessary). Subsequent reads / writes start at offset 0.

File.close()

Close the file handle.

File.readBytes(count)

Read up to count bytes from the current offset; advance. Returns the bytes as a String.

File.readBytes(count, offset)

Read at an absolute offset; do not advance the current offset.

File.read()

Read the entire file from offset 0.

File.writeBytes(s), writeBytes(s, offset)

Write s at the current or given offset.

File.write(s)

Replace file contents with s.

File.readLine()

Read from the current offset to the first LF (0x0A) or EOF and return the bytes read with any trailing LF removed. Advances the offset past the LF on a hit, or to EOF if none was found. Returns null when the offset is already at EOF; a blank line returns the empty string.

File.writeLine(s)

Write s at the current offset, then append an LF (0x0A). The offset advances past the LF. No special handling if s already ends in LF — the trailing LF is appended unconditionally; use writeBytes for raw control over line termination.

File.offset, offset=(o)

Current read/write position.

File.size

Length of the file in bytes.

File.isOpen

true while the handle is valid.

File.sha1, File.md5

Hashes of the file’s full content; raw digest bytes (20 / 16 bytes) returned as a Wren String. Implementation memory-maps the file; zero-length files are hashed as the empty buffer. Returned as bytes rather than hex so they compare directly against SFTPEntry.hash from the sha1s@syncterm.net / md5s@syncterm.net SFTP extensions; format hex yourself if you need it for display.

A File whose backing file has been deleted is dead — every method aborts the calling fiber with "handle is dead" or "backing file no longer exists". Detection happens both actively (when the file is removed via Directory.delete from any handle to its parent) and on every operation (a per-call fexist() check catches deletions that bypassed Directory.delete). See the Handle staleness note in the Directory section for the full mechanism.

Hook

The dispatcher registry. See Hook Events for the contract on each method, and HookHandle for the return type.

Hook.onKey(fn), Hook.onInput(fn), Hook.onMouse(fn), Hook.onStatus(fn), Hook.every(ms, fn), plus the filtered variants Hook.onKey(key, fn), Hook.onInput(byte, fn), Hook.onMouse(event, fn), and Hook.onMatch(pattern, fn). Each call returns a HookHandle.

Platform

OS identification. No further OS surface (no shell exec, no stdio, no process model) is exposed.

Method Description

Platform.name

OS identifier as a String. Returns "Windows" on Windows; the uname(2) sysname field on POSIX hosts (typically "FreeBSD", "Linux", "Darwin", etc.); "Unknown" on anything else. Win32 is checked before POSIX, so Cygwin / MSYS report the native OS.

Timer

One-shot fiber resumption after a delay. Registers a fiber to receive a TimerElapsed event after the requested number of milliseconds; the event lands on the result queue and the fiber is resumed by the standard drainer once doterm() reaches it.

For recurring fire-and-forget callbacks (no fiber resume), use Hook.every(ms, fn) instead.

Method Description

Timer.trigger(fiber, ms)

Schedule fiber to be resumed with a TimerElapsed after ms milliseconds. Multiple pending entries per fiber are fine — each fires independently. Returns null. Throws on a non-numeric or negative ms, or when more than 32 timers are pending across the whole VM.

Common idiom — fire and immediately await:

import "syncterm" for Timer

Timer.trigger(Fiber.current, 250)
Fiber.yield()
// 250ms have passed, with the doterm() loop running normally

Multi-fire / event-loop dispatch:

import "syncterm" for Timer, SFTP, SFTPStat, SFTPError

Timer.trigger(Fiber.current, 100)        // spinner tick
SFTP.stat(Fiber.current, "/foo")          // stat in flight
while (true) {
  var x = Fiber.yield()
  if (x is TimerElapsed) {
    spinner.update()
    Timer.trigger(Fiber.current, 100)    // re-arm
  }
  if (x is SFTPStat)  break
  if (x is SFTPError) break
}

TimerElapsed is a marker class with no fields — the caller already knows what it scheduled; just dispatch on type.

SFTP

SSH-channel side-band file transfer. The full surface follows the fiber-arg async pattern (see Async Ops below): the foreign primitive captures a fiber and queues work on the SSH session’s recv thread; the result lands on the framework’s queue and the standard drainer resumes the fiber.

SFTP.<op>(fiber, args…​) returns null when the request was queued (yield to receive the result), or an SFTPError directly on synchronous failure (session is gone, OOM at the foreign-method site).

Method Description

SFTP.available

true while the SSH connection has a usable SFTP subsystem.

SFTP.pubdir

The remote pubdir@syncterm.net path advertised by the server, or null if the extension wasn’t negotiated.

SFTP.realpath(fiber, path)

Resolve path to an absolute server path. Resumes with String or SFTPError.

SFTP.stat(fiber, path)

Stat a remote path. Resumes with SFTPStat or SFTPError.

SFTP.opendir(fiber, path)

Open a directory for iteration. Resumes with SFTPHandle or SFTPError.

SFTP.readdir(fiber, handle)

Read the next batch of entries from an open directory handle. Resumes with List<SFTPEntry> or null (EOF) or SFTPError. Loop until null, then call SFTP.close.

SFTP.open(fiber, path, flags)

Open a file. flags is a bitmask from FileFlag (see below). Resumes with SFTPHandle or SFTPError.

SFTP.read(fiber, handle, offset, count)

Read up to count bytes from handle at offset. Resumes with the bytes as a String, null at EOF, or SFTPError.

SFTP.write(fiber, handle, offset, bytes)

Write bytes to handle at offset. All-or-nothing: resumes with null on success or SFTPError.

SFTP.close(fiber, handle)

Close a handle returned by open / opendir. Resumes with null or SFTPError. After close, the handle is dead — further reads / writes / closes on it abort the calling fiber.

SFTP.mkdir(fiber, path), SFTP.rmdir(fiber, path), SFTP.remove(fiber, path), SFTP.rename(fiber, oldpath, newpath)

Mutation ops. Each resumes with null on success or SFTPError.

FileFlag

OR-able bitmask constants for SFTP.open’s flags argument; each matches the corresponding `SSH_FXF_* wire constant.

Constant Wire value

FileFlag.read

0x01

FileFlag.write

0x02

FileFlag.append

0x04

FileFlag.creat

0x08

FileFlag.trunc

0x10

FileFlag.excl

0x20

SFTPEntry

One entry from SFTP.readdir.

Member Description

name

Filename relative to the parent directory.

longname

Server-formatted ls-style line (when the lname@syncterm.net extension was negotiated; null otherwise). Used as toString when present.

size

File size in bytes.

mtime

Modification time, POSIX seconds.

isDir

true if the entry is a directory.

hash

SHA-1 / MD5 digest bytes from sha1s@syncterm.net / md5s@syncterm.net, or null if neither extension was negotiated. Compares directly against File.sha1 / File.md5.

SFTPStat

Result of SFTP.stat.

Member Description

size

File size in bytes.

mtime

Modification time, POSIX seconds.

atime

Access time, POSIX seconds.

mode

SFTP-wire permission bits (Unix-style on POSIX servers).

uid

Owner user ID.

gid

Owner group ID.

SFTPHandle

Opaque server file/dir handle. Only SFTP.open / SFTP.opendir produce one; only SFTP.read / SFTP.write / SFTP.readdir / SFTP.close consume one. GC’d handles fire SFTP.close fire-and-forget as a safety net, but scripts should close explicitly.

SFTPError

Returned in place of the typed result on any failure. Two error layers — distinguish via code:

Member Description

code

sftp_err_code_t enum value. Non-zero means a library / transport-level failure (OOM, ABORTED, SEND_FAILED, …); zero means the server returned a STATUS reply with an error code, in which case serverStatus carries it.

serverStatus

SSH_FX_* wire status (e.g. SSH_FX_NO_SUCH_FILE, SSH_FX_PERMISSION_DENIED). Meaningful only when code == 0.

message

Human-readable diagnostic text accumulated by the library (may be null).

isTransient

true for failures that may succeed on retry (transport drops, aborts, OOM).

The toString override produces "SFTPError: <name>: <message>" for System.print / interpolation; useful for log-line debug.

Async Ops

Every SFTP.<op>(fiber, …​) and Timer.trigger(fiber, ms) follow the same fiber-arg pattern. The first argument is the fiber to resume with the result. Two common shapes:

Blocking-style — pass Fiber.current, yield right after:

var r = SFTP.realpath(Fiber.current, ".") || Fiber.yield()
// r is String or SFTPError

The || shortcut means: if the foreign returned an SFTPError synchronously (session is gone), r is that error and Fiber.yield() isn’t evaluated. Otherwise the yield resumes with the result.

Callback-style — pass Fiber.new {|r| …​ }, calling fiber doesn’t yield at all:

SFTP.realpath(Fiber.new {|r|
  // r is String or SFTPError
}, ".")

The framework calls fiber.call(result) on whichever fiber was passed. Useful from hooks (which mustn’t yield directly — see Hook Events) and for fan-out where the result handler is naturally a closure over its own state.

WOM

The Wren Object Model — round-trip serialisation between Wren values and a textual literal format. Output is a strict subset of valid Wren syntax (and therefore also pasteable into Wren source). Deserialisation runs a hardened C parser, not eval, so feeding untrusted text to WOM.deserialize cannot execute code.

Supported types:

Type Notes

Null

null

Bool

true / false

Num

Num.toString form; NaN / Infinity have no Wren literal and are rejected

String

quoted with Wren-style escapes; % is always escaped to \% so output never starts an interpolation

List

[a, b, c]

Map

{key: value, …​} — keys must be hashable Wren types (Bool, Num, String, Range, null)

Range

coerced to List via .toList; serialisation is one-way (the deserialised value is a List)

Sequence

any other Sequence coerces to List via .toList

Values of any other type abort serialisation in strict mode and are silently omitted (or replaced with null at the top level) in lossy mode. Cyclic Lists / Maps abort in both modes.

Member Description

WOM.serialize(value)

Strict compact form. [1,2,3] / {"a":1,"b":2}. Aborts the fiber on unsupported types, NaN/Infinity, or cycles.

WOM.serialize(value, indent)

Strict pretty-printed form. indent is a String inserted once per nesting level (e.g. " " for two-space, "\t" for tab). Same error behaviour as the one-arg form.

WOM.serializeLossy(value)

Compact form that silently omits unsupported items from Lists and Maps; a top-level unsupported value becomes "null". Cycles still abort.

WOM.serializeLossy(value, indent)

Pretty-printed lossy form.

WOM.deserialize(text)

Parse text and return the reconstructed value. Aborts the fiber with a message containing the byte offset on syntax errors, premature end of input, trailing garbage, or a List / Map used as a Map key. Tolerates arbitrary whitespace and trailing commas inside […​] / {…​}. Accepts exactly the string-escape set the Wren compiler itself recognises.

import "syncterm" for WOM

var data = {
  "name":  "syncterm",
  "items": [1, 2, [3, 4]],
  "flags": {"verbose": true, "depth": 7},
}

System.print(WOM.serialize(data))
// {"name":"syncterm","items":[1,2,[3,4]],"flags":{"verbose":true,"depth":7}}

System.print(WOM.serialize(data, "  "))
// {
//   "name": "syncterm",
//   "items": [
//     1,
//     2,
//     [
//       3,
//       4
//     ]
//   ],
//   "flags": {
//     "verbose": true,
//     "depth": 7
//   }
// }

var roundTripped = WOM.deserialize(WOM.serialize(data))
System.print(roundTripped["flags"]["depth"])     // 7

// Lossy mode skips unsupported entries instead of aborting.
var mixed = [1, Fiber.new {}, 2, Fiber.new {}, 3]
System.print(WOM.serializeLossy(mixed))          // [1,2,3]

Built-in UI Library

SyncTERM ships a pure-Wren widget library on top of the Modal Input primitive. It draws into Surfaces with no foreign calls per cell, composites them through a screen-sized backbuffer, and applies a final Screen.putRect per frame, so the whole thing runs without flicker even on slow remote BBSes. The visual style follows UIFC conventions: cyan-on-blue dialog frames, yellow titles, lightbar selection, drop shadows on modals, and a distinct cyan inactive palette for any pane that’s currently behind a modal.

A minimal session looks like:

import "ui" for App, Pane, ListView, Alert, Rect
import "syncterm" for Screen

var snap = Screen.save()

var app  = App.new()
var size = Screen.size
var pane = Pane.new()
pane.bounds  = Rect.new(2, 2, size[0] - 2, size[1] - 2)
pane.title   = "Pick one"
pane.focused = true
pane.onClose = Fn.new { app.quit() }
app.root.add(pane)

var list = ListView.new()
var ib   = pane.innerBounds
list.bounds = Rect.new(ib.x, ib.y, ib.w, ib.h)
list.items  = ["alpha", "beta", "gamma"]
list.onSelect = Fn.new {|i, item| Alert.show(app, "Picked: %(item)") }
pane.add(list)

app.runSync()
Screen.restore(snap)

App.run() and App.runSync() differ in how they pump events. run() parks a fiber on Input.nextEvent and yields between events — inbound bytes flow normally, Hook.every and Timer.trigger keep firing. runSync() blocks the VM on Input.next() instead; use it from contexts where doterm can’t dispatch back into the App, e.g. the Wren console (whose REPL itself runs inside a Hook.onKey).

Importing

Every public class is exported from individual ui_*.wren modules and re-exported from the convenience aggregator ui:

import "ui" for App, Pane, ListView, TextInput, Button,
               Checkbox, RadioGroup, SpinBox, MenuBar, StatusBar,
               Form, Alert, Confirm, Prompt, Help, PopStatus,
               Painter, Style, Theme, Glyphs,
               Widget, Container, Rect

If you only need a couple of classes, the per-topic modules cut load cost: import "ui_pane" for Pane, import "ui_popup" for Confirm, etc. They’re listed in UI Modules.

App

App is the root of a UI session. It owns:

  • root — a Container that hosts the foreground widget tree.

  • a modal stack — synchronous dialogs push themselves onto it via app.modal(widget), which blocks until the widget pops itself (typically from its own dismissWith_(value) helper).

  • a global keymap — app.bind(keyCode, fn) registers a hotkey that fires when no widget consumes the key. F1 is bound by default to showHelp.

  • a screen-sized backbuffer Surface and a one-shot screen capture (Screen.readRect) used as the backdrop behind everything. Areas not covered by widgets show that capture rather than a styled fill — UIFC convention.

Methods of interest:

  • run() / runSync() — enter the event loop. Save/restore mouse events and CustomCursor automatically. tickMs= schedules a periodic onTick_ callback (overridable; no-op by default).

  • quit() — break out of the loop. Widgets and global handlers call this from their key/mouse handlers.

  • modal(widget) — push a modal, drain events until it pops, return the widget so the caller can read its result.

  • popStatus(message) — show or clear a transient centered overlay that does not intercept input. Useful for "Working…​" status while blocking on Input.next or a long Wren computation. The overlay sits above the foreground widget tree but below the modal stack, so a dialog the user is actively working with isn’t obscured by an indicator behind it.

  • showHelp() — walks from the focused leaf up the parent chain to the first widget with helpText set, then opens a Help dialog with that text. No-op if nothing in the chain has help.

  • post() / post(value) — wake the App’s parked fiber from outside the input / timer paths. See "Externally-driven repaint" below.

  • onPost=(fn) — handler for posted values that aren’t the no-payload sentinel. See below.

  • theme= — install a custom Theme (see Theme).

Externally-driven repaint

The async run() parks on Input.nextEvent, so by default only keyboard / mouse / Timer.trigger can wake the App. A network-driven app (chat client, ticker, log viewer fed by remote bytes) needs a way for Hook.onInput to nudge the UI without faking a key press. That’s what post is for:

import "syncterm" for Hook
import "ui_app"   for App

var app   = App.new()
// ... build widget tree ...

Hook.onInput { |b|
  buffer.add(b)              // mutate state
  someWidget.markDirty()      // mark UI as needing repaint
  app.post()                  // wake the App so drainOnce_ runs again
  return false
}

app.run()

app.post() queues a fiber resumption with a no-payload sentinel; the next main-loop drain delivers it, drawAll_ runs at the top of drainOnce_, and the dirty widget repaints. The hook returns synchronously — post only enqueues, satisfying the hooks-must-run-synchronously rule.

app.post(value) carries an arbitrary Wren value. When a non- sentinel value arrives, onPost (if set) is called with it after the redraw. Pass anything you can recognise:

app.onPost = Fn.new {|v|
  if (v is String && v == "irc.PRIVMSG") chatPane.scrollToBottom()
}

Hook.onInput { |b|
  // ...parse, accumulate...
  if (gotPrivmsg) app.post("irc.PRIVMSG")
  return false
}

post is a no-op when the App isn’t running (no parked fiber). It’s not supported under runSync — the synchronous variant blocks on Input.next() at the C level, not on Fiber.yield, so a wake through the result queue won’t reach it. Use run() for any app that needs external wake-ups.

App is not itself a Widget but exposes effectiveTheme and markDirty() so widget tree-walks can terminate at it. Setting a widget’s parent to an App is supported and is what pushModal does internally.

Widget

Base class for everything paintable. Holds:

  • bounds — a Rect in 1-based screen coords.

  • parent — pointer to the containing Container or App.

  • theme= — a per-widget Theme override; otherwise inherited via effectiveTheme walking the parent chain.

  • focused, visible, focusable, dirty — boolean state flags.

  • helpText= — string surfaced by App.showHelp (F1).

  • shadow= — when true, the parent paints a drop shadow on the cells immediately right and below the widget’s bounds.

  • surface — the widget’s private Surface backbuffer, allocated lazily and resized when bounds changes.

Layer-aware painting: every widget tracks the App’s active layer state (whether it or an ancestor is the modal top of stack) and repaints itself when the cached state no longer matches. No tree-wide dirty pass is needed when modals push or pop — each widget notices on its next draw() and repaints with the inactive theme variant. Subclasses override onPaint_() to draw into surface; the base Widget.draw() calls onPaint_(), clears the dirty flag, and returns the surface.

Hardware cursor: cursorPos returns [x, y] in 1-based screen coords (or null); cursorVisible returns whether the cursor should be shown while focused. App reads both off the focused leaf each frame and applies them. TextInput reports a real cursor; everything else hides it.

tryHotkey(ev) is a parent-driven hotkey hook. Container.handle scans its children with this when a printable key fell through the focused child, so a typed letter can activate a button advertising that letter even when focus is elsewhere.

Container

Widget subclass that manages a child list, a focused-child index, and event dispatch. add(child) / remove(child) mutate the tree; the first focusable child added becomes focused automatically.

Focus traversal:

  • Tab / RightfocusNext(), ordered ring with wrap.

  • BackTab / LeftfocusPrev().

  • Up / Downspatial nearest-focusable scan. Picks the focusable child whose centre is above / below the current focus’s centre and minimises Manhattan distance. Ties (children on the same row) go to the lower-indexed sibling — i.e. tab order.

Spatial Up/Down does not wrap; it falls back to a hotkey scan if nothing matches. Widgets that need arrows for their own semantics (TextInput consumes Left/Right, ListView consumes Up/Down) catch the keys before they bubble.

Mouse: Container.hitTest(px, py) walks children top-to-bottom (last-added wins) and returns the deepest visible widget covering the point, or the Container itself when nothing under it does.

Pane

Container with a frame, optional title, [?] and [X] corner buttons, and an inset interior. The default look is UIFC: double- line frame, title in its own bar row with a horizontal separator underneath, both corner buttons enabled.

Configuration knobs:

  • framePreset="single" (default for Popup) or "double" (default for bare Pane). Switches the glyph family from ---- style to ==== style.

  • titleAsBar= — when true, the title sits inside the frame in its own row with a horizontal separator underneath. When false, the title is embedded in the top border (-| Title |-). UIFC list views use the bar form; Popup subclasses use the embedded form.

  • helpable= / closeable= — show or hide the [?] and [X] corner buttons. Help button calls onHelp if set, else App.showHelp(). Close button calls onClose if set.

  • onHelp=, onClose= — function callbacks for the corner buttons.

pane.innerBounds returns the drawable interior Rect after the frame, title row, and separator are accounted for — children should size themselves against this.

Pane.focused is a visual flag — set it from the App or a parent container when the pane is the foreground. It does NOT redirect keyboard focus; that still flows through `Container’s normal focused-child routing to a leaf widget inside.

ListView

Scrollable list of items with a selection cursor and an optional scrollbar. Items can be anything; rendering goes through formatItem(item, width) which subclasses can override for rich per-row formatting. Default is item.toString.

Setup:

var list = ListView.new()
list.bounds   = Rect.new(x, y, w, h)
list.items    = ["alpha", "beta", "gamma"]
list.onSelect = Fn.new {|i, item| /* ... */ }

Navigation: Up, Down, PageUp, PageDown, Home, End move the selection. Enter fires onSelect with (index, item). Mouse clicks select the row; wheel scrolls three rows; clicks on the scrollbar column step or jump proportionally.

Scrollbar layout knobs:

  • scrollbarSide="left" (UIFC default) or "right".

  • scrollbarSeparator= — when true, paints a | divider between the scrollbar column and the content area.

  • showScroll= — set to false to disable the scrollbar entirely.

The scrollbar is only painted when items.count > bounds.h.

TextInput

Single-line text edit field. Stores the value as a list of codepoint strings so the cursor indexes by codepoint and emoji / multibyte characters take one cell per codepoint.

var t = TextInput.new()
t.bounds   = Rect.new(x, y, w, 1)
t.value    = "hello"
t.maxLen   = 64                  // optional cap
t.onSubmit = Fn.new {|s| /* Enter */ }
t.onChange = Fn.new {|s| /* per-keystroke */ }

Keys: Left, Right, Home, End, Backspace, Delete / DelChar edit; Enter fires onSubmit; printable codepoints insert at the cursor. Tab, BackTab, Up, Down are NOT consumed so containers can use them for focus traversal. Mouse clicks position the cursor at the clicked column.

cursorVisible returns true and cursorPos reports the cell that corresponds to the current insertion point in screen coords — backends that show the hardware cursor track the input as the user types.

Button

Single-row "[ label ]" widget. Activates on Enter, Space, or mouse click within bounds. onPress= sets the activation callback. intrinsicWidth returns label.count + 4 for sizing parents.

var b = Button.new("OK")
b.bounds  = Rect.new(x, y, b.intrinsicWidth, 1)
b.onPress = Fn.new { app.popModal() }

hotkeyIdx= selects which label letter to highlight (default 0, the first letter). tryHotkey matches that letter case- insensitively against typed input, so a button labeled "Yes" with hotkeyIdx = 0 activates on Y from anywhere in the same container — even when focus is on a sibling.

Checkbox

Single-row "[X] Label" toggle. value is a Bool, label is a String. Space or Enter flips the value when focused; mouse click anywhere on the widget toggles too.

var c = Checkbox.new("Show shadows")
c.bounds   = Rect.new(x, y, c.intrinsicWidth, 1)
c.value    = true
c.onChange = Fn.new {|v| /* ... */ }

onChange fires only when the value actually changes — assigning the current value is a no-op. Theme roles: checkbox, checkbox.focused. Glyphs reused: check.on ( U+221A), check.off (space).

RadioGroup

Multi-row mutually-exclusive selector. Owns its own item list and selected index — the whole group is a single focusable widget.

var g = RadioGroup.new()
g.bounds   = Rect.new(x, y, w, h)
g.items    = ["None", "SSL", "SSH"]
g.selected = 0
g.onChange = Fn.new {|i, item| /* ... */ }

Two pointers: cursor (visually highlighted row) and selected (the committed value). Up/Down move the cursor; Home / End jump to ends; Space or Enter commits the cursor as the selection. Mouse click selects directly. onChange fires only when the selection actually changes. The cursor highlight only appears while the widget itself has focus — leaving the group shows just the filled glyph on the selected row, so a multi-field Form doesn’t confuse the user with a stale lightbar.

wrap= controls the edge behavior of Up/Down. Default true: Up at the top wraps to the bottom and vice versa; every press is consumed. When false, Up at the top and Down at the bottom return false instead, letting the parent Container move focus to the previous / next sibling. Form.addField flips this to false on RadioGroup children so vertical traversal escapes the group at its edges.

Theme roles: radio.item, radio.item.focused. Glyphs reused: radio.on ( U+2022), radio.off ( U+25CB).

SpinBox

Single-row numeric input with up/down step buttons. Layout: [ 42 ▲▼].

var s = SpinBox.new()
s.bounds   = Rect.new(x, y, w, 1)
s.min      = 0
s.max      = 100
s.step     = 1
s.value    = 50
s.onChange = Fn.new {|v| /* ... */ }

Keys: Up / + adds step; Down / - subtracts; PageUp / PageDown move ten steps; Home / End jump to min / max. Mouse wheel scrolls one step; clicks on the up/down arrow cells step ±1. value= clamps to [min, max]. Theme roles: spinbox, spinbox.focused. Glyphs reused: scrollbar.up, scrollbar.down.

MenuBar

Horizontal strip of activatable items, typically at the top of the screen. Each item is a [label, Fn] pair; activation calls the Fn. No nested submenus in v1 — pop a Popup from the callback if you want one.

var bar = MenuBar.new()
bar.bounds = Rect.new(1, 1, sz[0], 1)
bar.items  = [
  ["File", Fn.new { Alert.show(app, "...") }],
  ["Edit", Fn.new { /* ... */ }],
  ["Help", Fn.new { app.showHelp() }],
]

Keys: Left / Right move focus between items with wrap; Home / End jump to ends; Enter / Space activate the focused item; a typed letter (case-insensitive ASCII) matched against item labels' first characters activates that item. tryHotkey(ev) is exposed on the widget so a Container parent can route a letter pressed elsewhere into the bar’s matcher. Mouse click activates the clicked item; clicks on inter-item gaps are dropped. Theme roles: menubar, menubar.item, menubar.item.focused.

StatusBar

Single-row strip used at the bottom (or top) of the screen for status text and key hints. Not focusable.

var s = StatusBar.new()
s.bounds = Rect.new(1, sz[1], sz[0], 1)
s.text   = "F1 Help  Esc Quit"

// or — multi-segment:
s.segments = [
  ["F1 Help",   "left"],
  ["Connected", "center"],
  ["09:42",     "right"],
]

text= sets a single left-aligned string. segments= takes a list of [text, align] pairs where align is "left", "center", or "right". Left segments stack from the left edge with a 2-cell gap; right segments flush to the right edge inward; one center segment is centred in the remaining space. Setting either form overrides the other. Theme role: statusbar (black on cyan).

Form

Container subclass that lays out (label, widget) pairs in a vertical stack with right-aligned labels in a column on the left. Optional OK / Cancel buttons appear on the row below the last field when onSubmit / onCancel are wired.

var f = Form.new()
f.bounds = pane.innerBounds

f.addField("Name:", nameInput)
f.addField("Port:", portSpin)
f.addFieldH("Encryption:", encRadio, 3)    // 3 rows tall
f.addField("",  autoCheckbox)              // no label

f.onSubmit = Fn.new { /* read field values */ }
f.onCancel = Fn.new { app.quit() }
pane.add(f)
  • addField(label, widget) — queues a one-row field.

  • addFieldH(label, widget, h) — queues a multi-row field (e.g. RadioGroup).

  • clearFields() — drops every queued field and removes the widgets as children.

  • rowGap= — blank rows between fields. Default 0.

  • rowH= — default per-field height. Default 1.

The label column width auto-sizes to the longest label; widgets start one column past the longest label plus a 2-cell gap. Tab / BackTab traversal hits widgets in declaration order, then OK, then Cancel. Esc fires onCancel if wired.

onSubmit= / onCancel= are idempotent setters — assigning the same callback twice doesn’t double-add the OK / Cancel button, and assigning null removes the button.

Popup family

Modal dialogs built on Pane (single-line frame, embedded title, drop shadow) and pushed onto the App’s modal stack. Each show is synchronous: the App’s modal() pumps drain until the popup pops itself, then control returns with the result.

import "ui" for Alert, Confirm, Prompt

Alert.show(app, "Disk full")

if (Confirm.show(app, "Delete file?")) {
  // ...
}

var name = Prompt.show(app, "Your name?", "anonymous")
if (name != null) {
  // user submitted
}

Alert — single OK button; any key (or Enter / Esc / mouse click on OK) dismisses. show returns null.

Confirm — Yes / No buttons. Y / N letter shortcuts dismiss directly; Enter activates the focused button; Tab cycles focus. show returns true on Yes, false on No or Esc.

PromptTextInput plus OK / Cancel buttons. Enter in the input or on OK submits the value; Esc or Cancel returns null.

PopStatus is the non-modal companion: a centered frame with a message and no buttons, used by App.popStatus(message). It does not push onto the modal stack — purely decorative — and is dismissed with app.popStatus(null).

Help viewer

A modal scrollable text viewer for context help. Splits the supplied body on newlines and renders one row per line inside a Popup with its own scroll state.

import "ui" for Help

Help.show(app, "Help — Editor",
          "Up/Down  scroll\nEnter   close\n...")

Keys: Up, Down, PageUp, PageDown, Home, End scroll; Esc or Enter dismisses. Wheel scrolls three lines; scrollbar clicks/drags jump proportionally. Same scrollbar knobs as ListView (scrollbarSide=, scrollbarSeparator=).

App.showHelp (F1 by default) walks from the focused leaf up the parent chain looking for a widget with helpText set, then calls Help.show with the title "Help" and that text. Set helpText on whichever widget is the most-specific scope you want help to fall back to.

Theme, Style, Glyphs

A Theme bundles a role → Style map and a Glyphs (name → glyph) lookup. Style is a four-field tuple (font, legacyAttr, fgRgb, bgRgb); any field may be null to inherit from a parent role. Painting picks one of the two color paths based on what the backend supports — RGB on capable terminals, legacy attr otherwise.

Role cascade: Theme.style(role) walks dotted role suffixes, merging partial Styles at each level until every field is populated. "list.item.focused" falls back to "list.item" falls back to "default". "default" is the cascade terminator and must be a complete Style.

Inactive cascade: when a role ends in .inactive, the Theme walks a parallel chain — X.inactive, then parent(X).inactive, …, finally default.inactive. Falls through to default if the theme has no inactive variants at all. Widget.style(role) auto-suffixes .inactive whenever inActiveLayer is false, so a single layer-state flag cascades through frame, title, list rows, input field, button labels, and scrollbar without per-widget bookkeeping.

A widget lands in the inactive layer for either of two reasons:

  • It isn’t part of the App’s modal-top subtree (a modal is on top, and the widget is in the layer behind it).

  • An ancestor that gates focus visibility has focused == false. Widget.gatesActiveLayer defaults to false; Pane overrides it to true, so an unfocused Pane in a multi-pane layout dims every cell it owns AND every descendant’s surface — frame, title, bg fill, list rows all agree on active-vs-inactive without each subclass having to know.

Theme.default is the built-in default, memoized so someTheme == Theme.default works as identity equality. It mirrors the classic SyncTERM look: white-on-blue active palette, yellow frame /title, lightbar selection, gray-on-blue scrollbar, with a UIFC inactive palette of bright-white-on-cyan and bright-yellow-on-cyan lightbar.

Glyphs entries are either a single string or [primary, fallback] pairs — primary is the rich Unicode glyph, fallback is an ASCII-safe substitute. Cell storage is CP437, so primary glyphs MUST be in CP437 too — Cell.ch= substitutes ? for any codepoint not in the table. As a safety net, Glyphs[name] auto-promotes to the per-entry fallback when the primary’s first codepoint doesn’t encode in CP437 (probed via the foreign Codepage.encodes_(text)). Resolutions are cached per name so the encoding test runs once. Set theme.glyphs.asciiOnly = true to force the fallback for every entry that has one — useful when the user has selected a font that can’t render line-drawing characters at all.

Glyph families used by the library:

  • frame.* — single-line -- borders, plus tee.left/right, title.left/right, separator.

  • frame.double.* — double-line == borders. Pane switches to this family when framePreset = "double".

  • scrollbar.track, scrollbar.thumb, scrollbar.up, scrollbar.down — scrollbar pieces.

Painter

Painter is the low-level drawing toolbox. Every primitive takes a Surface as its first argument and mutates the cells in place; no calls reach Screen.writeRect from here. Coordinates inside a Surface are 0-based (0..width-1, 0..height-1); widgets that work in absolute screen coords convert at composition time.

The most useful entry points:

fill(s, rect, ch, style) Bulk-fill a Surface rect with a single character + Style.

text(s, x, y, str, style)
text(s, x, y, str, style, maxW)

Draw str on a single row. 6-arg form caps at maxW cells.

frame(s, rect, glyphs, style)
frame(s, rect, glyphs, style, prefix)

Box around rect using prefix.* glyphs. Default prefix "frame"; pass "frame.double" for the double-line family.

frameTitle(s, rect, glyphs, frameStyle, title, titleStyle, prefix)

Frame with a centered `+

Title

+` in the top border.

hline(s, x, y, len, ch, style)
vline(s, x, y, len, ch, style)

Single-row / single-column lines.

scrollbar(s, x, y, h, scrollTop, total, viewport, glyphs, trackStyle, thumbStyle)

Vertical scrollbar with always-visible up/down arrows on the top/bottom rows when h >= 3. Thumb stays inside the track between the arrows.

scrollbarClick(py, h, total, viewport, current)

Resolve a click at row py (0-based, scrollbar-local) into a new scrollTop. Top/bottom arrow rows step ±1; track clicks jump proportionally.

shadow(s, x0, y0, w, h)

Paint a UIFC-style drop-shadow on the cells around the rect: 2 columns wide on the right, 1 row tall on the bottom. Each shadow cell keeps its existing character and dims the attribute (legacy 0x08, RGB fg 0x202020 on bg 0x000000).

applyStyle(cell, style)

Apply a Style to an existing Cell, leaving null fields untouched. Exposed for callers that mutate a cell view directly.

UI Modules

The library is split across one module per topic, plus the convenience aggregator:

ui

Re-exports every public class.

ui_app

App.

ui_widget

Widget, Container, Rect.

ui_pane

Pane.

ui_list

ListView.

ui_input

TextInput.

ui_button

Button.

ui_checkbox

Checkbox.

ui_radio

RadioGroup.

ui_spinbox

SpinBox.

ui_statusbar

StatusBar.

ui_menubar

MenuBar.

ui_form

Form.

ui_popup

Popup, Alert, Confirm, Prompt, PopStatus.

ui_help

Help.

ui_draw

Painter.

ui_style

Style, Glyphs, Theme.

Demo gallery

scripts/ui_demo.wren is a playground for the library; run it from the Wren console (Ctrl+`):

import "ui_demo" for UiDemo
UiDemo.run()

Top level is a list of widget demos; pick one with Enter to launch it, Esc returns to the gallery. Each demo runs its own App via runSync (the console blocks the main loop, so doterm can’t dispatch through to a parked fiber); nesting is fine because every App save/restores Screen, mouse events, and CustomCursor on entry and exit.

Worked Example: Auto-respond to a Prompt

A small script that watches inbound text for known prompts and sends canned responses. Hook.onMatch is the right tool for this — it runs a streaming regex against the inbound byte stream and fires the callback on each match, no manual buffering required:

import "syncterm" for Hook, Conn

Hook.onMatch("Press any key to continue") { |m|
  Conn.send("\r")
}

Hook.onMatch("Logon: ") { |m|
  Conn.send("myhandle\r")
}

onMatch is passthrough-only, so the matched text always reaches the terminal — the callback just acts on the side (sending bytes back to the BBS, updating script state, etc.). When a prompt is preceded by colour codes, use Hook.onMatchClean instead so the escapes don’t break the literal match.

onMatch substring-matches anywhere in the stream, not anchored to line boundaries, so prompts that wait for input mid-line (like `Logon: `) work the same as prompts followed by a newline. See Hook Events for the regex grammar accepted, including which constructs aren’t supported (no character classes, no anchors, no backslash escapes).

Worked Example: Per-byte Inspection

Hook.onInput is the lower-level primitive — fires once per inbound byte before the byte reaches the terminal. Use it when you need to react to individual bytes rather than text patterns. Below: count BEL (0x07) bytes received over the session.

import "syncterm" for Hook

class Stats {
  static bells { __bells }
  static bells=(n) { __bells = n }
}
Stats.bells = 0

Hook.onInput { |b|
  if (b == 0x07) Stats.bells = Stats.bells + 1
  return false
}

Inspect the count from the Wren console (Ctrl+`) — Stats.bells returns the running total. Returning false lets the byte through to cterm; returning true would consume it (so the BEL never reaches the terminal and never beeps). Filtered variants — Hook.onInput(0x07, fn) — push the byte equality check to the C side, avoiding a Wren entry on every non-target byte.

CTerm Manual

CTerm terminal characteristics

End of line behaviour (wrapping):

The cursor is moved to the first character of the next line as soon as a character is written to the last column of the current line, not on the next character. A tab will wrap to the next line only if the current cursor position is the last character on the line. This behavior is often surprising to people who are used to VT emulators which implement the LCF as documented in [STD-070], who expect the cursor to "stick" in the last column until the next character is received.

There are two settable flags that will impact the default behaviour.

CSI ? 7 l will disable wrapping at the end of line completely, and any characters written to the last column will not move the cursor at all, overwriting the existing charater. Default behaviour can be restored with CSI ? 7 h.

If the CSI = 4 h sequence is received, CTerm will enable LCF mode as documented in [STD-070], and CSI = 4 l will restore default behaviour. CSI = 5 h will set LCF mode and disable CSI = 4 l, as well as cause LCF to remain enabled across an ESC c (RIS).

Specifically, the LCF will be set when displaying a printable character advances the cursor to the right margin, and cleared by any of the following being received: CSI ? 6 h, CSI ? 6 l, CSI ? 7 l, CSI @, CSI A, CSI B, CSI a CSI j, CSI H, CSI f, CSI I, CSI Y, CSI J, CSI K, CSI P CSI X, CSI r, ESC E, ESC M, CR, LF, BS, TAB Any normal printable character when the cursor is at the right margin (of the screen or scrollable area).

C0 Control characters

0x00 NUL (NUL)

In doorway mode, indicates that the next character is a literal character. The IBM CP437 character will be displayed. This allows ESC and other control characters to be placed on the screen.

SOURCE: [BANSI]

0x07 Bell (BEL)

Beep

0x08 Backspace (BS)

Non-destructive backspace. Moves cursor position to the previous column unless the current column is the first, in which case no operation is performed.

SOURCE: [ECMA-48]

0x09 Character Tabulation (HT)

Moves to the next character tabulation stop. Does not overwrite any characters in between. If there are no character tabulation stops left in the line, moves to the first position of the next line. If the starting position is on the last line, will perform a scroll, filling the new line at bottom with the current attribute.

Note
0x0B (VT) is NOT treated as a line tabulation control character. It is displayed as the CP437 glyph (♂). Use CVT (CSI Pn Y) for line tabulation.

SOURCE: [ECMA-48]

0x0A Line Feed (LF)

Move cursor position to same column of the next row. If current row is the last row, scrolls the screen up and fills the new row with the current attribute.

SOURCE: [ECMA-48]

0x0D Carriage Return (CR)

Move cursor position to column 1 of the current line

SOURCE: [ECMA-48]

0x1B Escape (ESC)

Introduces a control code. The ESC and the next byte together form the control code. If the control code is not valid, the ESC is ignored.

SOURCE: [ECMA-48]

nF Escape Sequences

nF Escape Sequences are in the following format:
ESC {SPACE to '/}{'0' to '~'}
There may be multiple characters from the {SPACE to '/'} before the terminating {'@' to '~'} character.

At present, CTerm does not support any nF escape sequences.

SOURCE: [ECMA-35]

Fp Escape Sequences (Private control functions)

Private control functions are in the following format:
ESC {'0' to '?'}

SOURCE: [ECMA-35]

Legal combinations not handled are silently dropped.

ESC : Escaped String Terminator (ESCST)

Reserved for use within STS transmitted content (FETM=INSERT mode). OSC 8 hyperlink sequences inside an SOS-framed STS response cannot use bare ST (ESC \) as their terminator because it would prematurely close the SOS frame. Instead, ESC : \ (0x1B 0x3A 0x5C) is used as a substitute terminator.

ESC : is an unassigned Fp private-use escape sequence (per [ECMA-35]). A conformant parser that encounters ESC : outside of STS content silently drops it (per the rule above). Within STS FETM=INSERT content, the host parser recognizes the three-byte sequence ESC : \ as "escaped ST" and converts it to ESC \ before replay.

ESC 7 Save Cursor (DECSC)

Saves the current cursor position same as CSI s

SOURCE: [VT102]

ESC 8 Restore Cursor (DECRC)

Restores the current cursor position same as CSI u

SOURCE: [VT102]

Fe Escape Sequences (Control functions in the C1 set)

Control codes are in the following format:
ESC {'@' to '_'} Legal combinations which are not handled are silently dropped.

ESC E Next Line (NEL)

Moves to the line home position of the next line. (Same as CR LF)

SOURCE: [ECMA-48]

ESC F Start of Selected Area (SSA)

Marks the active presentation position as the first character position of a selected area. The content from SSA to ESA (or to the cursor position, depending on TTM) is eligible for transmission when STS is received.

SSA and ESA are cleared by changes to scroll margins (DECSTBM, DECSLRM), origin mode (DECOM), or RIS.

Scrolling does not invalidate SSA — it marks a position in the presentation component, not the content at that position. When STS triggers, the terminal reads whatever content is currently at the marked positions.

SOURCE: [ECMA-48] (Section 8.3.138)

ESC G End of Selected Area (ESA)

Marks the active presentation position as the last character position (inclusive) of a selected area begun by SSA.

ESA is required when TTM is set to ALL (CSI 16 h). When TTM is CURSOR (the default), the cursor position defines the end instead.

If no ESA has been issued, the selected area extends to the last cell of the viewport.

SOURCE: [ECMA-48] (Section 8.3.47)

ESC H Character Tabulation Set (HTS)

Sets a character tabulation stop at the current column.

SOURCE: [ECMA-48]

ESC J Line Tabulation Set (VTS)

Sets a line tabulation stop at the current line. Line tabulation stops are used by CVT (CSI Pn Y) and can be cleared by TBC with parameter values 1, 4, or 5.

Line tabulation stops are at fixed row numbers and are not affected by scrolling. No default line tabulation stops are set.

SOURCE: [ECMA-48]

ESC M Reverse Line Feed (RI)

Move up one line

SOURCE: [ECMA-48]

ESC P Device Control String (DCS)

Begins a string consisting of the characters 0x08 - 0x0d and 0x20-0x7e, terminated by a String Terminator (ST)

If a byte outside the valid range (other than ESC) is received, the string is discarded and the byte is re-processed as normal input. This applies to all command strings: DCS, OSC, PM, and APC.

SOURCE: [ECMA-48]

ESC S Set Transmit State (STS)

Triggers transmission of the selected area content established by SSA (and optionally ESA) back to the host.

The response is framed as SOS CTerm:STS:<N>: <content> ST where <N> is 0 for FETM=INSERT (attributed) or 1 for FETM=EXCLUDE (text only).

With TTM=CURSOR (default, CSI 16 l): Content from SSA up to but excluding the cursor position is transmitted.

With TTM=ALL (CSI 16 h): Content from SSA through ESA (inclusive) is transmitted regardless of cursor position.

With FETM=INSERT (default, CSI 14 l): Characters are transmitted with SGR sequences for attribute changes, doorway mode encoding for C0 control characters in cells, and OSC 8 for hyperlink changes. The stream is valid ECMA-48 that can be replayed to reproduce the region (after escaped-ST expansion; see below).

Escaped ST in attributed content: OSC 8 sequences require ST (ESC \) as their terminator, but bare ST would prematurely close the SOS frame. Within STS FETM=INSERT content, OSC 8 sequences are terminated with ESC : \ (0x1B 0x3A 0x5C) instead of ESC \ (0x1B 0x5C). ESC : is an unassigned Fp private-use escape sequence (per ECMA-35) that any conformant parser silently drops. The host parser recognizes ESC : \ within STS content as "escaped ST" and converts it to ESC \ before replay.

With FETM=EXCLUDE (CSI 14 h): Only graphic characters are transmitted. C0 and DEL bytes in cells are replaced with SPACE.

The transmitted stream is a linear sequence of character positions in presentation order (left to right, top to bottom). No line boundary markers are emitted — the host slices by the known screen width.

If no SSA has been issued, or the eligible area is empty, an empty response is returned: SOS CTerm:STS:<N>: ST

STS automatically clears the transmit state after transmission completes.

SOURCE: [ECMA-48] (Section 8.3.145)

ESC X Start Of String (SOS)

As the above strings, but may contain any characters except a Start Of String sequence or a String Terminator sequence.

SOURCE: [ECMA-48]

ESC [ Control Sequence Introducer (CSI)

Introduces Control Sequences

ESC \ String Terminator (ST)

Ends a string.

SOURCE: [ECMA-48]

ESC ] Operating System Command (OSC)

Begins a string consisting of the characters 0x08 - 0x0d and 0x20-0x7e, terminated by a String Terminator (ST)

SOURCE: [ECMA-48]

ESC ^ Privacy Message (PM)

Begins a string consisting of the characters 0x08 - 0x0d and 0x20-0x7e, terminated by a String Terminator (ST) The string is currently ignored.

SOURCE: [ECMA-48]

ESC _ Application Program Command (APC)

Begins a string consisting of the characters 0x08 - 0x0d and 0x20-0x7e, terminated by a String Terminator (ST)

SOURCE: [ECMA-48]

Fs Escape Sequences (Standardized single control functions)

Standardized single control functions are in the following format:
ESC {'`' to '~'}

SOURCE: [ECMA-35]

Legal combinations not handled are silently dropped.

ESC c Reset to Initial State (RIS)

Resets all the terminal settings, clears the screen, and homes the cursor.

SOURCE: [ECMA-48]

Control Sequences

Control sequences start with the Control Sequence Introducer which is ESC [. CSI will be used to express this from now on.

Control sequences are in the following format:
CSI {'0' (ZERO) to '?'}{SPACE to '/'}{'@' to '~'}
There may be multiple characters from the {'0' (ZERO) to '?'} and {SPACE to '/'} before the terminating {'@' to '~'} character.

Legal combinations not handled are silently dropped. Illegal combinations are displayed.

Sequence Parameters

Parameters are expressed by the {'0' (ZERO) to '?'} character set.

Sequences which use parameters use decimal parameters separated by a ';'. The use of a ':' from the set is reserved.

If the parameter string begins with '<', '=', '>', or '?' then this is a non-standard extension to the ANSI spec.

Table 1. Sequence Paramters

Pn

Indicates a single numeric parameter

Pn1 ; Pn2

Two numeric parameters

Pn…​

Any number of numeric parameters

Ps

Single selective parameter

Ps1 ; Ps1

Two selective parameters

Ps…​

Any numer of selective parameters

If a default is defined, the parameter is optional

CSI Pn @ Insert Character(s) (ICH)

Defaults: Pn = 1
Moves text from the current position to the right edge Pn characters to the right, with rightmost characters going off-screen and the resulting hole being filled with the current attribute.

SOURCE: [ECMA-48]

CSI Pn SP @ Scroll Left (SL)

Defaults: Pn = 1
Shifts the contents of the screen left Pn columns(s) with leftmost columns going off-screen and the resulting hole being filled with the current attribute.

SOURCE: [ECMA-48]

CSI Pn A Cursor Up (CUU)

Defaults: Pn = 1
Moves the cursor position up Pn lines from the current position. Attempting to move past the screen boundaries stops the cursor at the screen boundary.

SOURCE: [ECMA-48]

CSI Pn SP A Scroll Right (SR)

Defaults: Pn = 1
Shifts the contents of the screen right Pn columns(s) with rightmost columns going off-screen and the resulting hole being filled with the current attribute.

SOURCE: [ECMA-48]

CSI Pn B Cursor Down (CUD)

Defaults: Pn = 1
Moves the cursor position down Pn lines from the current position. Attempting to move past the screen boundaries stops the cursor at the screen boundary.

SOURCE: [ECMA-48]

CSI Pn C Cursor Right (CUF)

Defaults: Pn = 1 Moves the cursor position right Pn columns from the current position. Attempting to move past the screen boundaries stops the cursor at the screen boundary.

SOURCE: [ECMA-48]

CSI Pn D Cursor Left (CUB)

Defaults: Pn = 1 Moves the cursor position left Pn columns from the current position. Attempting to move past the screen boundaries stops the cursor at the screen boundary.

SOURCE: [ECMA-48]

CSI Ps1 ; Ps2 sp D Font Selection (FNT)

Defaults: Ps1 = 0 Ps2 = 0 "sp" indicates a single space character. Sets font Ps1 to be the one indicated by Ps2. Currently four fonts are supported. Ps2 must be between 0 and 255. Not all output types support font selection. Only X11 and SDL currently do.

Table 2. Supported Ps1 values

0

Default font

1

Font selected by the high intensity bit when CSI ? 31 h is enabled

2

Font selected by the blink intensity bit when CSI ? 34 h is enabled

3

Font selected by both the high intensity and blink bits when both CSI ? 31 h and CSI ? 34 h are enabled

Table 3. Currently included fonts

0

Codepage 437 English

1

Codepage 1251 Cyrillic, (swiss)

2

Russian koi8-r

3

ISO-8859-2 Central European

4

ISO-8859-4 Baltic wide (VGA 9bit mapped)

5

Codepage 866 (c) Russian

6

ISO-8859-9 Turkish

7

haik8 codepage (use only with armscii8 screenmap)

8

ISO-8859-8 Hebrew

9

Ukrainian font koi8-u

10

ISO-8859-15 West European, (thin)

11

ISO-8859-4 Baltic (VGA 9bit mapped)

12

Russian koi8-r (b)

13

ISO-8859-4 Baltic wide

14

ISO-8859-5 Cyrillic

15

ARMSCII-8 Character set

16

ISO-8859-15 West European

17

Codepage 850 Multilingual Latin I, (thin)

18

Codepage 850 Multilingual Latin I

19

Codepage 885 Norwegian, (thin)

20

Codepage 1251 Cyrillic

21

ISO-8859-7 Greek

22

Russian koi8-r (c)

23

ISO-8859-4 Baltic

24

ISO-8859-1 West European

25

Codepage 866 Russian

26

Codepage 437 English, (thin)

27

Codepage 866 (b) Russian

28

Codepage 885 Norwegian

29

Ukrainian font cp866u

30

ISO-8859-1 West European, (thin)

31

Codepage 1131 Belarusian, (swiss)

32

Commodore 64 (UPPER)

33

Commodore 64 (Lower)

34

Commodore 128 (UPPER)

35

Commodore 128 (Lower)

36

Atari

37

P0T NOoDLE (Amiga)

38

mO’sOul (Amiga)

39

MicroKnight Plus (Amiga)

40

Topaz Plus (Amiga)

41

MicroKnight (Amiga)

42

Topaz (Amiga)

43

Prestel

44

Atari ST

45

RIPterm

Not all fonts are supported in all modes. If a font is not supported in the current mode, no action is taken, but there should be a non-zero 'Font Selection result' value in the Font State Report.

SOURCE: [ECMA-48]

CSI Pn E Cursor Next Line (CNL)

Defaults: Pn = 1
Moves the cursor to the first column of the line Pn down from the current position. Attempting to move past the screen boundaries stops the cursor at the screen boundary.

SOURCE: [ECMA-48]

CSI Pn F Cursor Preceding Line (CPL)

Defaults: Pn = 1
Moves the cursor to the first column of the row Pn up from the current position. Attempting to move past the screen boundaries stops the cursor at the screen boundary.

SOURCE: [ECMA-48]

CSI Pn G Cursor Character Absolute (CHA)

Defaults: Pn = 1
Movies the cursor to column Pn of the current row.

SOURCE: [ECMA-48]

CSI Pn1 ; Pn2 H Cursor Position (CUP)

Defaults: Pn1 = 1 Pn2 = 1
Moves the cursor to the `Pn2`th column of the `Pn1`th line.

SOURCE: [ECMA-48]

CSI Pn I Cursor Forward Tabulation (CHT)

Defaults: Pn = 1
Move the cursor to the Pn-th next tab stop. Basically the same as sending TAB Pn times.

SOURCE: [ECMA-48]

CSI Ps J Erase in Page (ED)

Defaults: Ps = 0
Erases from the current screen according to the value of Ps

0

Erase from the current position to the end of the screen.

1

Erase from the current position to the start of the screen.

2

Erase entire screen. As a violation of ECMA-048, also moves the cursor to position 1/1 as a number of BBS programs assume this behaviour.

Erased characters are set to the current attribute.

SOURCE: [ECMA-48], [BANSI]

CSI Ps K Erase in Line (EL)

Defaults: Ps = 0
Erases from the current line according to the value pf Ps

0

Erase from the current position to the end of the line.

1

Erase from the current position to the start of the line.

2

Erase entire line.

Erased characters are set to the current attribute.

SOURCE: [ECMA-48]

CSI Pn L Insert Line(s) (IL)

Defaults: Pn = 1
Inserts Pn lines at the current line position. The current line and those after it are scrolled down and the new empty lines are filled with the current attribute. If the cursor is not currently inside the scrolling margins, has no effect.

SOURCE: [ECMA-48]

CSI Pn M Delete Line(s) / "ANSI" Music (DL)

Defaults: Pn = 1 Deletes the current line and the Pn - 1 lines after it scrolling the first non-deleted line up to the current line and filling the newly empty lines at the end of the screen with the current attribute. If the cursor is not currently inside the scrolling margins, has no effect. If "ANSI" Music is fully enabled (CSI = 2 M), and no parameter is specified, performs "ANSI" music instead. See "ANSI" MUSIC section for more details.

SOURCE: [ECMA-48], [BANSI]

CSI = Ps M CTerm Set ANSI Music (CTSAM)

NON-STANDARD EXTENSION.
Defaults: Ps = 0
Sets the current state of ANSI music parsing. 0 - Only CSI | will introduce an ANSI music string. 1 - Both CSI | and CSI N will introduce an ANSI music string. 2 - CSI |, CSI N, and CSI M will all introduce an ANSI music string. In this mode, Delete Line will not be available.

CSI N BananaCom ANSI Music (BCAM)

"ANSI" Music / Not implemented. If "ANSI" Music is set to BananaCom (CSI = 1 M) or fully enabled (CSI = 2 M) performs "ANSI" music. See "ANSI" MUSIC section for more details.

SOURCE: [BANSI]

CSI Pn P Delete Character (DCH)

Defaults: Pn = 1
Deletes the character at the current position by shifting all characters from the current column + Pn left to the current column. Opened blanks at the end of the line are filled with the current attribute. If the cursor is not currently inside the scrolling margins, has no effect.

SOURCE: [ECMA-48]

CSI Pn S Scroll Up (SU)

Defaults: Pn = 1
Scrolls the screen up Pn lines. New lines emptied at the bottom are filled with the current attribute.

SOURCE: [ECMA-48]

CSI ? Ps1 ; Ps2 S XTerm Set or Request Graphics Attribute (XTSRGA)

If Ps1 is 2, and Ps2 is 1, replies with the graphics screen information in the following format: CSI ? 2 ; 0 ; Px ; Py S Where Px is the width of the screen in pixels and Py is the height.

SOURCE: [XTerm]

CSI Pn T Scroll Down (SD)

Defaults: Pn = 1
Scrolls all text on the screen down Pn lines. New lines emptied at the top are filled with the current attribute.

SOURCE: [ECMA-48]

CSI Pn X Erase Character (ECH)

Defaults: Pn = 1
Erase p1 characters starting at the current character. Will not erase past the end of line. Erased characters are set to the current attribute. This can erase across scroll margins.

SOURCE: [ECMA-48]

CSI Pn Y Cursor Line Tabulation (CVT)

Defaults: Pn = 1
Moves the cursor to the corresponding character position (same column) of the line corresponding to the Pn-th following line tabulation stop. Line tabulation stops are set by VTS (ESC J).

If there is no next line tabulation stop, the screen scrolls up one line and the cursor moves to the last row, preserving the current column.

SOURCE: [ECMA-48]

CSI Pn Z Cursor Backward Tabulation (CBT)

Defaults: Pn = 1
Move the cursor to the Pnth preceding tab stop. Will not go past the start of the line.

SOURCE: [ECMA-48]

CSI Pn ` Character Position Absolute (HPA)

Defaults: Pn = 1
Move the cursor to the specified position on the current row. Will not go past the end of the line.

SOURCE: [ECMA-48]

CSI Pn a Cursor Position Forward (HPR)

Defaults: Pn = 1
Moves the cursor position forward Pn columns from the current position. Attempting to move past the screen boundaries stops the cursor at the screen boundary.

SOURCE: [ECMA-48]

CSI Pn b Repeat (REP)

Defaults: Pn = 1
Repeats the previous graphic character Pn times. Will not repeat escape sequences.

SOURCE: [ECMA-48]

CSI Ps c Device Attributes (DA)

Defaults: Ps = 0
If Ps is 0, CTerm will reply with the sequence: CSI = 67;84;101;114;109;pN c 67;84;101;114;109 is the ASCII values of the "CTerm" string. pN is the revision ID of CTerm with dots converted to semi-colons (e.g. "1;156"). Use the revision to detect if a specific feature is available. If you are adding features to a forked version of cterm, please do so by adding an extra parameter to the end, not by incrementing any existing one!

SOURCE: [ECMA-48]

CSI < Ps c CTerm Device Attributes (CTDA)

Defaults: Ps = 0
If Pn is 0, CTerm will reply with the sequence: CSI < 0 ; Ps…​ c

Table 4. Possible values for Ps

1

Loadable fonts are availabe via Device Control Strings

2

Bright Background (ie: DECSET 32) is supported

3

Palette entries may be modified via an Operating System Command string

4

Pixel operations are supported (currently, sixel and PPM graphics)

5

The current font may be selected via CSI Ps1 ; Ps2 sp D

6

Extended palette is available

7

Mouse is available

CSI PN d Line Position Absolute (VPA)

Defaults: Pn = 1
Moves to row specified by Pn.

SOURCE: [ECMA-48]

CSI Pn SP d Tab Stop Remove (TSR)

Defaults: None
Removes a tab stop at postion Pn.

SOURCE: [ECMA-48]

CSI Pn e Line Position Forward (VPR)

Defaults: Pn = 1
Moves forward Pn rows.

SOURCE: [ECMA-48]

CSI Pn1 ; Pn2 f Character and Line Position (HVP)

Defaults: Pn1 = 1 Pn2 = 1
Moves the cursor to the Pn2th column of the Pn1th line.

SOURCE: [ECMA-48]

CSI Ps g Tabulation Clear (TBC)

Defaults: Ps = 0
Clears tabulation stops according to the value of Ps:

0

Clears the character tabulation stop at the current position.

1

Clears the line tabulation stop at the current line.

3

Clears all character tabulation stops.

4

Clears all line tabulation stops.

5

Clears all tabulation stops (both character and line).

SOURCE: [ECMA-48]

CSI Ps…​ h Set Mode (SM)

Sets one or more ANSI modes. The following modes are supported:

14

FORMAT EFFECTOR TRANSFER MODE (FETM) = EXCLUDE. When set, transmitted data (via STS) contains only graphic characters. C0/DEL bytes in cells are replaced with SPACE. SGR, positioning, and other formator functions are excluded.

16

TRANSFER TERMINATION MODE (TTM) = ALL. When set, the complete selected area from SSA to ESA is transmitted regardless of cursor position. When reset (default), only content preceding the cursor is transmitted (cursor position excluded).

SOURCE: [ECMA-48] (Sections 7.2.6, 7.2.18)

CSI = 255 h (BCSET)

NON-STANDARD EXTENSION
Enable DoorWay Mode

SOURCE: [BANSI]

CSI = 4 h Enable Last Column Flag (CTELCF)

NON-STANDARD EXTENSION
Enable Last Column Flag mode

CSI = 5 h Force Last Column Flag (CTFLCF)

NON-STANDARD EXTENSION
Force Last Column Flag mode

CSI ? Ps…​ h Set Mode (DECSET)

NON-STANDARD EXTENSION
Sets one or more mode. The following modes are supported:

6

Enable origin mode.

In this mode, position parameters are relative to the top left of the scrolling region, not the screen. Defaults to reset.

SOURCE: [VT102]

7

Enable auto wrap

This is the normal mode in which a write to the last column of a row will move the cursor to the start of the next line triggering a scroll if required to create a new line. Defaults to set.

SOURCE: [VT102]

9

X10 compatible mouse reporting

Mouse button presses will send a CSI M <button> <x> <y> Where <button> is ' ' + button number (0-based) <x> and <y> are '!' + position (0-based)

SOURCE: [XTerm]

25

Display the cursor. Defaults to set.

SOURCE: [VT320]

31

Enable bright alt character set

With this mode set, the bright (1) graphic rendition selects characters from an alternate character set. Defaults to reset.

32

Bright Intensity Disable

This makes the bright intensity bit not control the intensity. Mostly for use with CSI ? 31 h to permit fonts in the same colours. Defaults to reset.

33

Blink to Bright Intensity Background

With this mode set, the blink (5,6) graphic renditions cause the background colour to be high intensity rather than causing blink. Defaults to reset.

34

Enable blink alt character set

With this mode set, the blink (5, 6) graphic renditions selects characters from an alternate character set. Defaults to reset

35

Blink Disabled

This makes the blink (5, 6) graphic renditions not cause the character to blink. Mostly for use with CSI ? 34 h to permit fonts to be used without blinking. Defaults to reset.

67

When set, the backspace key sends a backspace character.

Defaults to set.

69

DEC Left Right Margin Mode enabled

Enables CSI s to set the left/right margins, and disables CSI s from saving the current cursor position.

80

Sixel Scrolling Enabled

When this is set, the sixel active position begins in the upper-left corner of the currently active text position. When the sixel active position reaches the bottom of the page, the page is scrolled up. At the end of the sixel string, a sixel newline is appended, and the current cursor position is the one in which the bottom sixel is in. Defaults to set.

SOURCE: [VT330/340]

1000

Normal tracking mode mouse reporting

Mouse button presses will send a CSI M <button> <x> <y> Where <button> is ' ' + button number (0-based) Mouse button releases will use a button number of 4 <x> and <y> are '!' + position (0-based)

SOURCE: [XTerm]

1001

Highlight tracking mode mouse reporting

(Not supported by SyncTERM)

SOURCE: [XTerm]

1002

Button-event tracking mode mouse reporting

Mouse button presses and movement when a button is pressed will send a CSI M <button> <x> <y> Where <button> is ' ' + button number (0-based) 32 is added to the button number for movement events. Mouse button releases will use a button number of 4 <x> and <y> are '!' + position (0-based)

SOURCE: [XTerm]

1003

Any-event tracking mode mouse reporting

Mouse button presses and movement will send a CSI M <button> <x> <y> Where <button> is ' ' + button number (0-based) 32 is added to the button number for movement events. Mouse button releases will use a button number of 4 <x> and <y> are '!' + position (0-based) If no button is pressed, it acts as though button 0 is.

SOURCE: [XTerm]

1004

Focus-event tracking mode mouse reporting

(Not supported by SyncTERM)

SOURCE: [XTerm]

1005

UTF-8 encoded extended coordinates

(Not supported by SyncTERM)

SOURCE: [XTerm]

1006

SGR encoded extended coordinates

Instead of the CSI M method, the format of mouse reporting is changed to CSI < Pb ; Px ; Py M for presses and CSI < Pb ; Px ; Py m for releases. Instead of CSI M Px and Py are one-based. Pb remains the same (32 added for movement) Button 3 is not used for release (separate code)

SOURCE: [XTerm]

1007

Alternate scroll mode

(Not supported by SyncTERM)

SOURCE: [XTerm]

1015

URXVT encoded extended coordinates

(Not supported by SyncTERM)

SOURCE: [XTerm]

2004

Set bracketed paste mode

SOURCE: [XTerm]

CSI Pn j Character Position Backward (HPB)

Defaults: Pn = 1
Moves the cursor position left Pn columns from the current position. Attempting to move past the screen boundaries stops the cursor at the screen boundary.

SOURCE: [ECMA-48]

CSI Pn k Line Position Backward (VPB)

Defaults: Pn = 1 Moves the cursor position up Pn lines from the current position. Attempting to move past the screen boundaries stops the cursor at the screen boundary.

SOURCE: [ECMA-48]

CSI Ps…​ l Reset Mode (RM)

Resets ANSI modes. Same parameter values as SM above. Resetting mode 14 restores FETM=INSERT (attributed transmission). Resetting mode 16 restores TTM=CURSOR (cursor-exclusive termination).

SOURCE: [ECMA-48]

CSI = 255 l Disable DoorWay Mode (BCRST)

NON-STANDARD EXTENSION

SOURCE: [BANSI]

CSI = 4 l (CTDLCF)

NON-STANDARD EXTENSION
Disable Last Column Flag mode

CSI ? Ps…​ l Reset Mode (DECRST)

NON-STANDARD EXTENSION
Resets one or more mode. The following modes are supported:

6

Origin Mode

With this mode reset, position parameters are relative to the top left of the screen, not the scrolling region. Defaults to reset.

SOURCE: [VT102]

7

Disable auto wrap

Resetting this mode causes a write to the last column of a to leave the cursor where it was before the write occurred, overwriting anything which was previously written to the same position.

SOURCE: [VT102]

9

Disable X10 compatible mouse reporting

25

Hide the cursor. Defaults to set.

SOURCE: [VT320]

31

Disable bright alt character set

With this mode reset, the bright (1) graphic rendition does not select an alternative font. Defaults to reset.

32

Bright Intensity Enable

When reset, bright intensity graphics rendition behaves normally. Defaults to reset.

33

Disable Blink to Bright Intensity Background

With this mode set, the blink (5,6) graphic renditions do not affect the background colour. Defaults to reset.

34

Disable blink alt character set

With this mode reset, the blink (5, 6) graphic renditions do not select characters from an alternate character set. Defaults to reset.

35

Blink Enable

With this mode reset, the blink (5,6) graphic renditions behave normally (cause the characters to blink). Defaults to reset.

67

When reset, the backspace key sends a delete character.

Defaults to set.

69

DEC Left Right Margin Mode disabled

Disables CSI s from setting the left/right margins, and changes it back to saving the current cursor position. The current left/right margins are maintained.

80

Sixel Scrolling Disabled

When this is reset, the sixel active position begins in the upper-left corner of the page. Any commands that attempt to advance the sixel position past the bottom of the page are ignored. At the end of the sixel string, the current cursor position is unchanged from where it was when the sixel string started. Defaults to set.

SOURCE: [VT330/340]

1000

Disable Normal tracking mode mouse reporting

SOURCE: [XTerm]

1001

Disable Highlight tracking mode mouse reporting

(Not supported by SyncTERM)

SOURCE: [XTerm]

1002

Disable Button-event tracking mode mouse reporting

SOURCE: [XTerm]

1003

Disable Any-event tracking mode mouse reporting

SOURCE: [XTerm]

1004

Disable Focus-event tracking mode mouse reporting

(Not supported by SyncTERM)

SOURCE: [XTerm]

1005

Disable UTF-8 encoded extended coordinates

(Not supported by SyncTERM)

SOURCE: [XTerm]

1006

Disable SGR encoded extended coordinates

SOURCE: [XTerm]

1007

Disable Alternate scroll mode (Not supported by SyncTERM)

SOURCE: [XTerm]

1015

Disable URXVT encoded extended coordinates

(Not supported by SyncTERM)

SOURCE: [XTerm]

2004

Disable bracketed paste mode

SOURCE: [XTerm] [Paste64]

CSI Ps…​ m Select Graphic Rendition (SGR)

Defaults: Ps1 = 0
Sets or clears one or more text attributes. Unlimited parameters are supported and are applied in received order. The following are supported:

Ps

Description

Blink

Bold

FG

BG

TF

TB

0

Default attribute, white on black

1

Bright Intensity

2

Dim intensity (clears the bright attribute; equivalent to SGR 22 in PC text mode since there is no distinct dim state)

5

Blink (By definition, slow blink)

6

Blink (By definition, fast blink)

NOTE: Both blinks are the same speed.

7

Negative Image - Reverses FG and BG

8

Concealed characters, sets the

foreground colour to the background colour.

22

Normal intensity

25

Steady (Not blinking)

27

Positive Image - Restores FG and BG

NOTE: This should be a separate attribute than 7 but this implementation makes them equal

30

Black foreground

31

Red foreground

32

Green foreground

33

Yellow foreground

34

Blue foreground

35

Magenta foreground

36

Cyan foreground

37

White foreground

38

Extended Foreground (see notes)

39

Default foreground (same as white)

40

Black background

41

Red background

42

Green background

43

Yellow background

44

Blue background

45

Magenta background

46

Cyan background

47

White background

48

Extended Background (see notes)

49

Default background (same as black)

91

Bright Red foreground

92

Bright Green foreground

93

Bright Yellow foreground

94

Bright Blue foreground

95

Bright Magenta foreground

96

Bright Cyan foreground

97

Bright White foreground

100

Bright Black background

101

Bright Red background

102

Bright Green background

103

Bright Yellow background

104

Bright Blue background

105

Bright Magenta background

106

Bright Cyan background

107

Bright White background

All others are ignored.

Blink indicates the blink bit. Bold indicates the bold bit. FG indicates the foreground colour. BG indicates the background colour. TF indicates that the Tru Colour foreground is changed. TB indicates that the Tru Colour background is changed.

Note
For 90-97, there is no effect unless bright foreground colours are enabled (i.e., DECSET mode 32 is not set, which is the default).
Note
For 100-107, there is no effect unless bright background colours are enabled via DECSET mode 33.
Note
For 38 and 48, two additional formats are supported, a palette selection and a direct colour selection.

For palette selection, an additional two parameters are required after that value. They are considered part of the 38/48, not separate values. The first additional parameter must be a 5. The second additional parameter specified the palette index to use. To set the foreground to orange, and the background to a fairly dark grey, you would send: CSI 38 ; 5 ; 214 ; 48 ; 5 ; 238 m

The default palette is the XTerm 256-colour palette. [256colors]

For direct colour selection, an additional four parameters are required after that value. They are considered part of the 38/48, not separate values. The first additional parameter must be a 2. The second, third, and fourth specify the R/G/B values respectively. CTerm handles this with an internal temporary palette, so scrollback may not have the correct colours. The internal palette is large enough for all cells in a 132x60 screen to have unique foreground and background colours though, so the current screen should always be as expected.

SOURCE: [ECMA-48], [XTerm]

CSI Ps n Device Status Report (DSR)

Defaults: Ps = 0
A request for a status report. CTerm handles the following three requests:

5

Request a DSR

CTerm will always reply with CSI 0 n indicating "ready, no malfunction detected"

6

Request active cursor position

CTerm will reply with CSI y ; x R where y is the current line and x is the current row. When origin mode (CSI ? 6 h) is enabled, the reported position is relative to the scroll region, not the screen. When the Last Column Flag is set, the reported column is the last column (not last column + 1).

255

NON-STANDARD EXTENSION (BCDSR)

Replies as though a CSI 6 n was received with the cursor in the bottom right corner. i.e.: Returns the terminal size as a position report.

SOURCE: [ECMA-48] (parameters 5 and 6 only) [BANSI] (parameter 255)

CSI = Ps n State/Mode Request/Report (CTSMRR)

NON-STANDARD EXTENSION
Defaults: Ps = 1
When Ps is 1, CTerm will respond with a Font State Report of the form CSI = 1 ;pF ;pR ;pS0 ;pS1 ;pS2 ;pS3 n pF is the first available loadable-font slot number pR is the result of the previous "Font Selection" request:

0

successful font selection

1

failed font selection

99

no font selection request has been received

pS0 - pS3 contain the font slots numbers of previously successful "Font Selection" requests into the 4 available alternate-font style/attribute values:

pS0

normal attribute font slot

pS1

high intensity foreground attribute font slot

pS2

blink attribute font slot

pS3

high intensity blink attribute font slot

When Ps is 2, CTerm will respond with a Mode Report of the form CSI = 2[;pN [;pN] […​]] n Where pN represent zero or more mode values set previously (e.g. via CSI ? pN h). Mode values cleared (disabled via CSI ? pN l) will not be included in the set of values returned in the Mode Report. If no modes are currently set, an empty parameter will be included as the first and only pN.

When Ps is 3, CTerm will respond with a Mode Report of the form CSI = 3 ; pH ; pW n Where pH is the height of a character cell in pixels, and pW is the width of a character cell in pixels.

When Ps is 4, CTerm will respond with a Mode Report of the form CSI = 4 ; pF n Where pF is 1 if LCF mode is enabled, and 0 if it is disabled.

When Ps is 5, CTerm will respond with a Mode Report of the form CSI = 5 ; pF n Where pF is 1 if LCF mode is forced, and 0 if it is not.

When Ps is 6, CTerm will respond with a Mode Report of the form CSI = 6 ; pA n Where pA is 1 if OSC 8 hyperlink support is available, and 0 if it is not.

CSI ? Ps [ ; Pn ] n Device Status Report (DECDSR)

When Ps is 62 (DECMSR) and there is no Pn, CTerm will respond with a Mode Report of the form CSI 32767 * { This indicates that 524,272 bytes are available for macro storage. This is not actually true, SyncTERM will use all available memory for macro storage, but some software checks this value, and some parsers don’t allow more than INT16_MAX parameter values.

When Ps is 63 (DECCKSR) Pn defaults to 1, and CTerm will respond with a checksum of the defined macros in the form DCS Pn ! xxxx ST Where xxxx is the hex checksum.

SOURCE: [VT420]

CSI Ps $ p Request Mode — ANSI (DECRQM)

Requests the current state of ANSI mode Ps. The terminal responds with CSI Ps ; Pm $ y (DECRPM).

The following ANSI modes can be queried:

1

GATM — permanently reset (Pm=4)

2

KAM — permanently reset (Pm=4)

3

CRM — permanently reset (Pm=4)

4

IRM — permanently reset (Pm=4)

5

SRTM — permanently reset (Pm=4)

6

ERM — permanently reset (Pm=4)

7

VEM — permanently reset (Pm=4)

8

BDSM — permanently reset (Pm=4)

9

DCSM — permanently reset (Pm=4)

10

HEM — permanently reset (Pm=4)

11

PUM — permanently reset (Pm=4)

12

SRM — permanently reset (Pm=4)

13

FEAM — permanently reset (Pm=4)

14

FETM — changeable (Pm=1 when set, Pm=2 when reset)

15

MATM — permanently reset (Pm=4)

16

TTM — changeable (Pm=1 when set, Pm=2 when reset)

17

SATM — permanently reset (Pm=4)

18

TSM — permanently reset (Pm=4)

21

GRCM — permanently set (Pm=3)

22

ZDM — permanently set (Pm=3)

All ECMA-48 standard modes are reported. Modes 14 and 16 are changeable; modes 21 and 22 are permanently set; all others are permanently reset. Any unrecognized mode returns Pm=0.

SOURCE: [VT320], [ECMA-48]

CSI = Ps $ p Request Mode — CTerm Extension

NON-STANDARD EXTENSION
Requests the current state of CTerm private mode Ps. The terminal responds with CSI = Ps ; Pm $ y using the same Pm values as DECRQM above.

The following CTerm modes can be queried:

4

Last Column Flag mode (CTELCF)

5

Forced Last Column Flag (CTFLCF) — reports 3 (permanently set) when forced

255

DoorWay mode

CSI ? Ps $ p Request Mode — Private (DECRQM)

Requests the current state of private mode Ps. The terminal responds with a mode report of the form CSI ? Ps ; Pm $ y (DECRPM) where Pm indicates the mode state:

0

Mode not recognized

1

Set

2

Reset

3

Permanently set

4

Permanently reset

The following private modes can be queried:

6

Origin mode (DECOM)

7

Auto-wrap mode (DECAWM)

9

X10 mouse reporting

25

Cursor visible (DECTCEM)

31

Alternate character set

32

No bright foreground

33

Bright background

34

Blink = alternate character set

35

No blink

67

Backarrow key mode (DECBKM)

69

Left/right margin mode (DECLRMM)

80

Sixel scrolling

1000–1007, 1015

Mouse tracking modes

2004

Bracketed paste mode

Any unrecognized mode returns Pm = 0.

SOURCE: [VT320]

CSI Ps SP q Set Cursor Style (DECSCUSR)

Defaults: Ps = 1 Sets the cursor style

Ps Result

0

Blinking block

1

Blinking block

2

Steady block

3

Blinking underline

4

Steady underline

SOURCE: [VT520]

CSI Pn1 ; Pn2 r Set Top and Bottom Margins (DECSTBM)

Defaults: Pn1 = 1 Pn2 = last line on screen
Selects top and bottom margins, defining the scrolling region. Pn1 is the line number of the first line in the scrolling region. Pn2 is the line number of the bottom line.

SOURCE: [VT100]

CSI Pt ; Pl ; Pb ; Pr ; Ps …​ Ps $ r Change Attributes in Rectangular Area (DECCARA)

Defaults: Pt = 1 Pl = 1 Pb = last line Pr = last column
Sets SGR attributes in a rectangular screen area without changing characters. Pt, Pl, Pb, Pr define the rectangle. The remaining Ps parameters are SGR attribute values (same as CSI m), including extended colour sequences (38;5;N, 38;2;R;G;B, 48;5;N, 48;2;R;G;B).

If DECSACE is set to 1 (stream mode), the operation applies as a character stream from (Pt,Pl) to (Pb,Pr) wrapping at line boundaries. In stream mode, Pl > Pr is permitted when Pt < Pb.

Coordinates are affected by DECOM (origin mode). Does not change cursor position.

SOURCE: [VT420]

CSI Ps1 ; Ps2 * r Select Communication Speed (DECSCS)

Set the output emulation speed. If Ps1 or Ps2 are omitted, causes output speed emulation to stop Ps1 may be empty. Sequence is ignored if Ps1 is not empty, 0, or 1. The value of Ps2 sets the output speed emulation as follows:

Value Speed

empty, 0

Unlimited

1

300

2

600

3

1200

4

2400

5

4800

6

9600

7

19200

8

38400

9

57600

10

76800

11

115200

SOURCE: [VT420]

CSI Pn1 ; Pn2 s Set Left and Right Margins (DECSLRM)

(Only when DEC Left Right Margin Mode - 69 - is enabled)

Defaults: Pn1 = 1 Pn2 = last column on screen
If either Pn1 or Pn2 is zero, the current setting is retained. Selects left and right margins, defining the scrolling region. Pn1 is the column number of the first column in the scrolling region. Pn2 is the column number of the right column.

SOURCE: [XTerm]

CSI s Save Current Position (SCOSC)

(Only when DEC Left Right Margin Mode - 69 - is disabled) NON-STANDARD EXTENSION Saves the current cursor position for later restoring with CSI u although this is non-standard, it’s so widely used in the BBS world that any terminal program MUST implement it.

SOURCE: [ANSISYS]

CSI ? Ps…​ s Save Mode Setting (CTSMS)

NON-STANDARD EXTENSION
Saves the current mode states as specified by CSI ? l and CSI ? h. If Ps1 is omitted, saves all such states. If one or more values of Ps is included, saves only the specified states (arguments to CSI ? l/h).

CSI Ps ; Pn1 ; Pn2 ; Pn3 t Select a 24-bit colour (CT24BC)

NON-STANDARD EXTENSION

If Ps is 0, sets the background colour. If Ps is 1, sets the foreground colour. Pn1, Pn2, Pn3 contains the RGB value to set. CTerm handles this with an internal temporary palette, so scrollback may not have the correct colours. The internal palette is large enough for all cells in a 132x60 screen to have unique foreground and background colours though, so the current screen should always be as expected.

CSI Pt ; Pl ; Pb ; Pr ; Ps …​ Ps $ t Reverse Attributes in Rectangular Area (DECRARA)

Defaults: Pt = 1 Pl = 1 Pb = last line Pr = last column
Toggles (reverses) SGR attributes in a rectangular screen area without changing characters. Pt, Pl, Pb, Pr define the rectangle. The remaining Ps parameters specify which attributes to toggle:

Ps Action

0

Invert all toggleable attributes (bold, blink, negative)

1 or 22

Toggle bold (XOR legacy attribute bit 3)

5 or 25

Toggle blink (XOR legacy attribute bit 7)

7 or 27

Toggle negative (swap foreground and background)

All other Ps values are silently ignored.

If DECSACE is set to 1 (stream mode), the operation applies as a character stream wrapping at line boundaries.

Coordinates are affected by DECOM (origin mode). Does not change cursor position.

SOURCE: [VT420]

CSI u Restore Cursor Position (SCORC)

Move the cursor to the last position saved by CSI s. If no position has been saved, the cursor is not moved.

SOURCE: [ANSISYS]

CSI ? Ps…​ u Restore Mode Setting (CTRMS)

NON-STANDARD EXTENSION

Restores the mode states as saved via CSI ? s. If Ps is omitted, restores all such states. If one or more values of Ps is included, restores all the specified states (arguments to CSI ? l/h)

CSI Pts ; Pls ; Pbs ; Prs ; Pps ; Ptd ; Pld ; Ppd $ v Copy Rectangular Area (DECCRA)

Copies a rectangular area of characters from one location to another. The copied text retains its character values, attributes, and hyperlinks.

Pts, Pls, Pbs, Prs define the source rectangle (top, left, bottom, right). Pps is the source page (ignored — single page). Ptd, Pld define the destination top-left corner. Ppd is the destination page (ignored).

Defaults: Pts = 1, Pls = 1, Pbs = last line, Prs = last column, Ptd = 1, Pld = 1.

If Pbs < Pts or Prs < Pls, DECCRA is ignored. Coordinates are affected by origin mode (DECOM) but not by margins. Values exceeding page dimensions are clamped. The cursor does not move. If the destination area extends past the screen, the off-screen portion is clipped.

SOURCE: [VT420]

CSI 2 $ w Request Tab Stop Report (DECTABSR)

Requests a list of tab stops. The list is in the form: DCS 2 $ u Pt ST

The string Pt is a list of tab stops separated by `/`s.

SOURCE: [VT320]

CSI Pch ; Pt ; Pl ; Pb ; Pr $ x Fill Rectangular Area (DECFRA)

Fills a rectangular area with the character specified by Pch. The fill character uses the visual attributes set by the most recent SGR command. Hyperlinks are cleared on filled cells.

Pch is the decimal value of the fill character. Any value from 0x20 to 0x7E or 0x80 to 0xFF is accepted. If Pch is in the C0 range (0x00–0x1F) or is DEL (0x7F), the command is ignored.

Pt, Pl, Pb, Pr define the rectangle (top, left, bottom, right). Defaults: Pt = 1, Pl = 1, Pb = last line, Pr = last column.

If Pb < Pt or Pr < Pl, DECFRA is ignored. Coordinates are affected by origin mode (DECOM) but not by margins. Values exceeding page dimensions are clamped. The cursor does not move.

SOURCE: [VT420]

CSI Ps * x Select Attribute Change Extent (DECSACE)

Controls whether DECCARA and DECRARA operate on a rectangular area or a character stream.

Ps Mode

0

Rectangular (default)

1

Stream (linear character sequence wrapping at line boundaries)

2

Rectangular

Reset to 0 by RIS.

SOURCE: [VT420]

CSI Pn1 ; Ps ; Pn2 ; Pn3 ; Pn4 ; Pn5 * y Request Checksum of Rectangular Area (DECRQCRA)

Returns a checksum for the specified rectangular area. Pn1 is an ID that is returned in the response. Ps MUST be 1 Pn2 specifies the top row of the rectangle Pn3 specifies the left column of the rectangle Pn4 specifies the bottom row of the rectangle Pn5 specifies the right column of the rectangle The return value is in the format of DCS Pn1 ! ~ xxxx ST Where xxxx is the hex value of the checksum.

Source: [VT420]

CSI Pt ; Pl ; Pb ; Pr $ z Erase Rectangular Area (DECERA)

Erases all character positions in the specified rectangular area. Erased positions are set to SPACE with the visual attributes from the most recent SGR command. Hyperlinks are cleared on erased cells.

Pt, Pl, Pb, Pr define the rectangle (top, left, bottom, right). Defaults: Pt = 1, Pl = 1, Pb = last line, Pr = last column.

If Pb < Pt or Pr < Pl, DECERA is ignored. Coordinates are affected by origin mode (DECOM) but not by margins. Values exceeding page dimensions are clamped. The cursor does not move.

SOURCE: [VT420]

CSI Pn * z Invoke Macro (DECINVM)

Invokes a macro. Pn specifies the macro number. If Pn is not 0..63, no action is taken.

SOURCE: [VT420]

CSI = Ps1 ; Ps2 { (CTOSF)

NON-STANDARD EXTENSION (Deprecated)
Defaults: Ps1 = 255 Ps2 = 0
Indicates that a font block is following. Ps1 indicates the font slot to place the loaded font into. This must be higher than the last default defined font (See CSI sp D for list of predefined fonts) Ps2 indicates font size according to the following table:

0

8x16 font, 4096 bytes.

1

8x14 font, 3584 bytes.

2

8x8 font, 2048 bytes.

The DCS font string should be used instead as of CTerm 1.213

CSI Ps $ } Select Active Status Display (DECSASD)

Defaults: Ps = 0
Selects which display subsequent output is directed to. Ps = 0 sends output to the main display (default); Ps = 1 sends it to the one-row status line. Ps = 1 is silently ignored unless the status display type is currently host-writable (DECSSDT Ps = 2).

When the status line is the active display, writes land in a single-row sub-terminal with its own cursor, SGR attributes, and saved-cursor slot. Line-feed, wrap-to-next-line, and scroll do not leave the status row. Selecting Ps = 0 restores the main display’s cursor and resumes normal output there.

Switching back to Ps = 0 is also forced implicitly whenever DECSSDT changes to a different type while the status display was active.

SOURCE: [VT320]

CSI Ps $ ~ Select Status Display Type (DECSSDT)

Defaults: Ps = 1
Selects the status display type. Ps = 0 hides the status row and extends the main display by one line. Ps = 1 shows SyncTERM’s native indicator (clock, connection state, help hints) — this is the default and matches the pre-existing user preference (overridable from the BBS list or the -n CLI flag). Ps = 2 gives the row to the host: SyncTERM’s indicator is cleared and the row becomes writable via DECSASD.

The main display is resized in place across toggles; cursor position and existing screen content are preserved. Values of Ps outside 0..2 are silently ignored.

SOURCE: [VT320]

CSI Pn ' } Insert Column (DECIC)

Defaults: Pn = 1
Inserts Pn blank columns into the scrolling region, starting at the column that has the cursor. Columns between the cursor and the right margin shift to the right. Columns shifted past the right margin are lost. The inserted columns are filled with SPACE using the current SGR attributes. Hyperlinks are cleared on inserted cells.

DECIC has no effect if the cursor is outside the scrolling margins. The cursor does not move.

SOURCE: [VT420]

CSI Pn ' ~ Delete Column (DECDC)

Defaults: Pn = 1
Deletes Pn columns from the scrolling region, starting at the column that has the cursor. The remaining columns between the cursor and the right margin shift to the left. Blank columns are added at the right margin, filled with SPACE using the current SGR attributes. Hyperlinks are cleared on the new blank cells.

DECDC has no effect if the cursor is outside the scrolling margins. The cursor does not move.

SOURCE: [VT420]

Device Control Strings

A Device Control String Begins with a DCS and ends with a ST The following commands are supported:

DCS CTerm:Font:p1:<b64> ST CTerm Loadable Font (CTLF)

Indicates the string is a loadable font. (CTerm 1.213)

p1 is a font slot number, which must be higher than the last default defined font (See CSI sp D for list of predefined fonts). <b64> is the base64 encoded font data. Font size is deduced from the size of the data. This replaces the now deprecated CSI = Ps1 ; Ps2 {

DCS [ p1 [ ; p2 ] ] q ST Sixel Sequence

Defaults: p1 = 0 p2 = 0 Indicates the string is a sixel sequence.

p1 selects the vertical height of a single pixel. This may be overridden by the raster attributes command, and is deprecated. Supported values

Table 5. Supported Values of p1
Value Vertical Size

0,1,5,6

2 pixels

2

5 pixels

3,4

3 pixels

7,8,9

1 pixel

p2 indicates if unset sixels should be set to the current background colour. If p2 is 1, positions specified as 0 remain at their current colour.

Any additional parameters are ignored.

The rest of the string is made up of sixel data characters and sixel control functions. Sixel data characters are in the range of ? (0x3f) to ~ (0x7e). Each sixel data character represents six vertical pixels. The data is extracted by subtracting 0x3f from the ASCII value of the character. The least significant bit is the topmost pixel.

Table 6. Sixel Control Functions
Function Parameters Name Description

!

Pn X

Graphics Repeat Introducer

The character X is repeated Pn times.

"

p1 ; p2 [ ; p3 [ ; p4 ] ]

Raster Attributes

p1 indicates the vertical size in pixels of each sixel.
p2 indicates the horizontal size in pixels.
p3 and p4 define the height and width (in sixels) respectively of a block to fill with the background colour. This block may not extend past the current bottom of the screen. If any pixel data characters proceed this command, it is ignored.

#

p1

Colour Select

Selects the current foreground colour from the sixel palette.

#

p1 ; p2 ; p3 ; p4 ; p5

Palette map

Defines sixel palette entry p1 and sets it as the current foreground colour.
p2 specifies the colour space to define the colour in, the only supported value is 2.
p3, p4, and p5 specify the red, green, and blue content as a percentage (0-100).

$

Graphics Carriage Return

Returns the active position to the left border of the same sixel row. Generally, one pass per colour is used. In passes after the first one, sixels with a value of zero are not overwritten with the background colour.

-

Graphics New Line

Moves the active position to the left border of the next sixel row.

SOURCE: [VT330/340]

DCS $ q pt ST Request Status String (DECRQSS)

pt is the intermediate and/or final characters of a control function to query the status of. The terminal will send a response in the format

DCS p1 $ r pt ST

p1 is 1 if the terminal supports querying the control function and 0 if it does not.

pt is the characters in the control function except the CSI characters. If p1 is zero, pt is zero-length.

Table 7. Currently supported values of pt:
pt Description

m

Request SGR parameters

r

Request top and bottom margins

s

Request left and right margins

t

Request height in lines

$|

Request width in columns

*|

Request height in lines

` q` (space + q)

Request cursor style (DECSCUSR). The response contains the current cursor style number (0–4) as set by CSI Ps SP q.

*r

Request communication speed (DECSCS). The response contains the current speed parameters as ;Ps2*r where Ps2 is the speed index (0=default, 1=300, 2=600, …​, 11=115200).

*x

Request attribute change extent (DECSACE). The response contains the current DECSACE value (0 or 1).

$~

Request status display type (DECSSDT). The response contains the current type (0 = none, 1 = indicator, 2 = host-writable).

$}

Request active status display (DECSASD). The response contains the current selection (0 = main, 1 = status).

SOURCE: [VT420]

DCS p1 [ ; p2 [ ; p3 ] ! z ST Define Macro (DECDMAC)

Defaults: p2 = 0 p3 = 0

Sets a macro to be replayed using CSI Pn * z

p1 is the macro number to set, and must be between 0 and 63 inclusive.

If p2 is zero, the macro numbered p1 will be deleted before the new macro is set. If p2 is one, all macros are deleted before the new macro is set. If the macro is zero length, only the delete action is stored, you can’t store a zero-length macro.

If p3 is zero, the macro is defined using ASCII characters (0x20 - 0x7e and 0xa0 - 0xff only). Note that since ESC (0x1b) is not in this range, ASCII-mode macros cannot contain escape sequences; use hex-mode (p3 = 1) instead. If p3 is one, the macro is defined using hex pairs.

When the macro is defined using hex pairs, a repeat sequence may be included in the format of ! Pn ; D..D ; Pn specifies the number of repeats (default of one instance)+ D..D is the sequence of pairs to send Pn times. The terminating ; may be left out if the sequence to be repeated ends at the end of the string.

SOURCE: [VT420]

Operating System Commands

An Operating System Command Begins with an OSC and ends with a ST The following commands are supported:

OSC 4;(pX;pY)…​ ST Palette Redefinition/Query (OSC 4)

Specifies one or more palette redefinitions or queries.
pX is the palette index, and pY is the colour definition
Color format: rgb:R/G/B:: Where R, G, and B are a sequence of one to four hex digits representing the value of the red, green, and blue channels respectively.

If pY is ?, CTerm responds with the current colour for palette index pX in the form OSC 4;pX;rgb:RR/GG/BB ST where RR, GG, and BB are two hex digit colour values reflecting the 8-bit-per-channel storage precision.

SOURCE: [XTerm]

OSC 104 [ ; Ps …​ ] ST Reset Palette Entry (OSC 104)

Resets palette entry to default. If the entire string is "104" (ie: no Ps present), resets all colours. Otherwise, only each index separated by a semicolon is reset. SOURCE: [XTerm]

OSC 10 ; ? ST Query Default Foreground Color (OSC 10)

Queries the default foreground color.
CTerm responds with OSC 10;rgb:RR/GG/BB ST where RR, GG, and BB are two hex digit color values.

SOURCE: [XTerm]

OSC 11 ; ? ST Query Default Background Color (OSC 11)

Queries the default background color.
CTerm responds with OSC 11;rgb:RR/GG/BB ST where RR, GG, and BB are two hex digit color values.

SOURCE: [XTerm]

Sets a hyperlink on subsequent text output. Text printed after this sequence will be associated with the given URI. An empty URI ends the hyperlink region.

The params field is a colon-separated list of key=value pairs. The id= parameter allows non-contiguous text runs to share the same hyperlink.

Only http, https, ftp, and ftps URIs are supported. Other schemes are silently ignored.

Users can open hyperlinks by clicking when BBS mouse capture is off, or by Ctrl+clicking when mouse capture is active.

Examples
ESC ] 8 ; ; https://example.com ST Click here ESC ] 8 ; ; ST
ESC ] 8 ; id=link1 ; https://example.com ST part1 ESC ] 8 ; ; ST ... ESC ] 8 ; id=link1 ; https://example.com ST part2 ESC ] 8 ; ; ST

Application Program Commands

An Application Program Command begins with an APC and ends with a ST SyncTERM implements the following APC commands:

APC SyncTERM:VER ST Get SyncTERM Version (CTSV)

SyncTERM responds with an APC string of the form APC SyncTERM:VER;version ST where version is the full version string of SyncTERM, either SyncTERM 1.7rc1 for release builds or SyncTERM 1.7b Debug (Sep 27 2025) for debug builds

APC SyncTERM:C;S Ps1 Ps2 ST Store file (CTSFI)

Where Ps1 is a filename and Ps2 is the base64 encoded contents of the file. The named file is stored in the cache directory for the current connection.

APC SyncTERM:C;L [ ; Ps] ST List Files (CTLFI)

Defaults: Ps = *
Ps is the glob(3) pattern to use matching files.
SyncTERM responds with an APC string with lines separated by newlines. The first line is always SyncTERM:C;L\n and for each matching file, a line in the form <Filename> TAB <MD5 sum> LF is sent (ie: "coolfont.fnt\t595f44fec1e92a71d3e9e77456ba80d1\n")

APC SyncTERM:C;SetFont; Pn ; Ps ST Set Font (CTSF)

Where Pn is a font slot number (max 255) and Ps is a filename in the cache. This sets font slot Pn to use the specified font file.

APC SyncTERM:C;DrawPPM Ps…​ Ps1 ST Draw a PPM from Cache (CTDPFC)

Draws a PPM from the cache directory on the screen.
Ps1 is the filename and is required. Arguments for Ps are optional. The following options can be included (separated by semi-colons):

Table 8. Currently Supported Ps Arguments:
Argument Description

SX=#

Sets the left X position in the specified image to copy from. Default = 0.

SY=#

Sets the top Y position in the specified image to copy from. Default = 0.

SW=#

Sets the width of the portion of the image to copy. Default = Image width - SX

SH=#

Sets the height of the portion of the image to copy. Default = Image height - SH

DX=#

Sets the X position on the screen to draw the image at. Default = 0.

DY=#

Sets the Y position on the screen to draw the image at. Default = 0.

MX=#

Sets the X position in the mask to start applying from. Default = 0.

MY=#

Sets the Y position in the mask to start applying from. Default = 0.

MW=#

Sets the overall width of the mask (not the width to apply). If MFILE is not specified, and a mask is (ie: using MASK=), this is required. If MFILE is specified, the width is read from the file.

MH=#

Sets the overall height of the mask (not the height to apply). If MFILE is not specified, and a mask is (ie: using MASK=), this is required. If MFILE is specified, the width is read from the file.

MFILE=<filename>

Specifies a filename in the cache directory of a PBM file specifying a mask of which pixels to copy. Any pixel set to black (ie: 1) in the PBM will be drawn from the source image. Pixels set to white (ie: 0) will be left untouched.

MASK=<maskbits>

Specifies a base64-encoded bitmap, each set bit will be drawn from the source image, cleared bits will not be drawn. Requires MW= and MH= to be specified.

MBUF

Uses the loaded mask buffer.

The PPM file may be raw (preferred) or text. SyncTERM does not support more than 255 values per colour channel and assumes it is correctly using the BT.709 gamma transfer.

APC SyncTERM:C;DrawJXL Ps…​ Ps1 ST Draw a JPEG XL from Cache (CTDJFC)

Draws a JPEG XL from the cache directory on the screen.
Ps1 is the filename and is required. Arguments for Ps are optional. The following options can be included (separated by semi-colons):

Table 9. Currently Supported Ps Arguments:
Argument Description

SX=#

Sets the left X position in the specified image to copy from. Default = 0.

SY=#

Sets the top Y position in the specified image to copy from. Default = 0.

SW=#

Sets the width of the portion of the image to copy. Default = Image width - SX

SH=#

Sets the height of the portion of the image to copy. Default = Image height - SH

DX=#

Sets the X position on the screen to draw the image at. Default = 0.

DY=#

Sets the Y position on the screen to draw the image at. Default = 0.

MX=#

Sets the X position in the mask to start applying from. Default = 0.

MY=#

Sets the Y position in the mask to start applying from. Default = 0.

MW=#

Sets the overall width of the mask (not the width to apply). If MFILE is not specified, and a mask is (ie: using MASK=), this is required. If MFILE is specified, the width is read from the file.

MH=#

Sets the overall height of the mask (not the height to apply). If MFILE is not specified, and a mask is (ie: using MASK=), this is required. If MFILE is specified, the width is read from the file.

MFILE=<filename>

Specifies a filename in the cache directory of a PBM file specifying a mask of which pixels to copy. Any pixel set to black (ie: 1) in the PBM will be drawn from the source image. Pixels set to white (ie: 0) will be left untouched.

MASK=<maskbits>

Specifies a base64-encoded bitmap, each set bit will be drawn from the source image, cleared bits will not be drawn. Requires MW= and MH= to be specified.

MBUF

Uses the loaded mask buffer.

APC SyncTERM:C;LoadPPM Ps…​ Ps0 ST Load a PPM to Buffer (CTLPTB)

Loads a PPM to a buffer. Ps0 is the filename

Table 10. Currently Supported Ps Arguments:

Argument

Description

B=#

Selects the buffer (0 or 1 only) to paste from.

APC SyncTERM:C;LoadJXL Ps…​ Ps0 ST Load a JPEG XL to Buffer (CTLJTB)

Loads a JPEG XL to a buffer. Ps0 is the filename

Table 11. Currently Supported Ps Arguments:

Argument

Description

B=#

Selects the buffer (0 or 1 only) to paste from.

APC SyncTERM:C;LoadPBM Ps…​ Ps0 ST Load a PBM to Buffer (CTLPBTB)

Loads a PBM to a buffer. Ps0 is the filename

APC SyncTERM:P;Copy Ps…​ ST Copy Screen into Buffer (CTCSIB)

Copies a portion of the screen into an internal pixel buffer for use with the Paste function. Defaults to copying the entire screen. All coordinates and dimensions are in pixels.

Table 12. Currently Supported Ps Arguments:

Argument

Description

B=#

Selects the buffer (0 or 1 only) to copy to.

X=#

Sets the left X position on the screen to start copying at. Default = 0.

Y=#

Sets the top Y position on the screen to start copying at. Default = 0.

W=#

Sets the width to copy. Default = Screen width - X.

H=#

Sets the height to copy. Default = Screen height - X.

APC SyncTERM:P;Paste Ps…​ ST Paste Buffer to Screen (CTPBTS)

Pastes from the copied pixel buffer. Supports the same options as the Cache DrawPPM command except for the filename, and adds the B= option. All coordinates and dimensions are in pixels.

Table 13. Currently Supported Ps Arguments:

Argument

Description

SX=#

Sets the left X position in the specified image to copy from. Default = 0.

SY=#

Sets the top Y position in the specified image to copy from. Default = 0.

SW=#

Sets the width of the portion of the image to copy. Default = Image width - SX

SH=#

Sets the height of the portion of the image to copy. Default = Image height - SH

DX=#

Sets the X position on the screen to draw the image at. Default = 0.

DY=#

Sets the Y position on the screen to draw the image at. Default = 0.

MX=#

Sets the X position in the mask to start applying from. Default = 0.

MY=#

Sets the Y position in the mask to start applying from. Default = 0.

MW=#

Sets the overall width of the mask (not the width to apply). If MFILE is not specified, and a mask is (ie: using MASK=), this is required. If MFILE is specified, the width is read from the file.

MH=#

Sets the overall height of the mask (not the height to apply). If MFILE is not specified, and a mask is (ie: using MASK=), this is required. If MFILE is specified, the width is read from the file.

MFILE=<filename>

Specifies a filename in the cache directory of a PBM file specifying a mask of which pixels to copy. Any pixel set to black (ie: 1) in the PBM will be drawn from the source image. Pixels set to white (ie: 0) will be left untouched.

MASK=<maskbits>

Specifies a base64-encoded bitmap, each set bit will be drawn from the source image, cleared bits will not be drawn. Requires MW= and MH= to be specified.

MBUF

Uses the loaded mask buffer.

B=#

Selects the buffer (0 or 1 only) to paste from.

APC SyncTERM:Q;JXL ST Query JXL Support (CTQJS)

Queries support for the JXL image format.
SyncTERM will respond with a CTerm APC State Report of the form

CSI = 1 ; pR - n

pR is 0 if JXL support is not available, and 1 if it is.

APC SyncTERM:Q; Ps ST Feature presence query (CTQF)

Generic feature-availability probe. Ps is the feature name; currently defined:

Feature Meaning

libsndfile

Runtime-loaded libsndfile for audio file decoding (APC SyncTERM:A;Load). Under the dynamic (WITH_SNDFILE) build mode, the first query also triggers a lazy xp_dlopen attempt — no harm if the library is absent, just reports unavailable. The STATIC_SNDFILE build mode always reports available; WITHOUT_SNDFILE always reports unavailable.

The response is an audio DSR (see CSI = 7 n under "Audio APCs"), with the feature ID (100 for libsndfile) and availability (0 / 1) as the pair. Example responses:

  • libsndfile available: CSI = 7 ; 100 ; 1 n

  • libsndfile unavailable: CSI = 7 ; 100 ; 0 n

Audio APCs

SyncTERM exposes a BBS-facing audio playback API on top of its internal mixer. The model has two pieces:

Patch slots — 256 per-session buffers of S16 stereo 44100 Hz PCM, addressed by slot number 0..255. Slots are filled by Load (decoding a file via libsndfile), Synth (in-terminal tone generator), or Copy (duplicating an existing slot).

Channels — 16 mixer streams, addressed 0..15. Channels 0 and 1 alias cterm’s internal music stream (ANSI/BananANSI/SyncTERM music) and foreground-SFX stream (RIP / OOII) respectively; the BBS can use Flush, Volume, Wait, and Update on them but cannot Queue audio onto them. Channels 2..15 are APC-dedicated and lazy-opened at -12 dB base level on first use.

A Queue moves a slot’s buffer into a channel’s FIFO, after which the slot is empty. The mixer plays channels concurrently with sample-accurate volume, fade-in/out envelopes, loop, and crossfade.

Parameter letters

Keys are single characters (C, S, V, etc.) in a ;-delimited Key=Value list. Two keys (X and L) are presence flags — their mere inclusion in the token list sets the flag; they take no value.

Letter Meaning Used by

S

Slot number (0..255) — patch source or dest

Load, Synth, Queue, Copy

D

Destination slot (Copy only)

Copy

W

Waveform / shape

Synth

F

Frequency in Hz

Synth

T

Time / duration (ms default, see below)

Synth

C

Channel number (0..15)

Queue, Flush, Volume, Wait, Update

I

Fade-in duration

Queue

O

Fade-out duration

Queue, Flush

X

Crossfade (presence flag, no value)

Queue

L

Loop (presence flag, no value)

Queue

V

Volume, both channels

Queue, Volume

VL

Volume, left channel

Queue, Volume

VR

Volume, right channel

Queue, Volume

Duration grammar for T, I, O:

  • No suffix → milliseconds (T=500 = 500 ms).

  • ms suffix → explicit milliseconds.

  • f suffix → frames, sample-exact (T=22050f = half a second @ 44.1 kHz).

  • p suffix → full periods of F, zero-crossing aligned. Valid only in Synth’s T; rejected for Queue/Flush fade durations (which apply to pre-rendered buffers with no single frequency).

Any parse error (unknown suffix, empty, non-numeric) yields a duration of 0 — Synth with T=0 just produces an empty slot; Queue with I=0 skips that envelope.

Volume grammar for V, VL, VR:

  • No suffix → linear percentage 0..100 (100 = 0 dB unity, 50 ≈ -6 dB, 0 = silence clamped to -60 dB).

  • dB suffix (case-insensitive) → explicit dB value, parsed as a float with any sign (V=-6.5dB).

Volume parameters on Queue apply to that buf only (summed with the stream’s base dB during playback). Volume verb updates the channel’s base level for subsequent playback.

Errors are silent. The BBS can confirm an action succeeded by querying CSI = 7 ; <ch> n afterward and inspecting the state.

APC SyncTERM:A;Load;S=N;<filename> ST Load patch from Store (CTAL)

Decodes <filename> (a file previously uploaded via SyncTERM:C;S) into slot N using libsndfile. Silently ignored if libsndfile is not available — query SyncTERM:Q;libsndfile first to confirm.

APC SyncTERM:A;Synth;S=N;W=<shape>;F=<hz>;T=<dur> ST Synthesize patch (CTAS)

Generates a waveform into slot N. Shapes: SIN, SAW, SQ, SINE_HARM, SINE_SAW, SINE_SAW_CHORD, SINE_SAW_HARM, SILENCE. F=0 or W=SILENCE produces silence regardless of other parameters.

APC SyncTERM:A;Copy;S=N;D=M ST Copy patch (CTAC)

Deep-copies slot N into slot M. Source slot is NOT emptied.

APC SyncTERM:A;Queue;C=#;S=N;…​ ST Queue patch on channel (CTAQ)

Moves slot N’s buffer onto channel `C’s FIFO. Slot becomes empty. Rejected when `C=0 or C=1 (those channels are cterm-owned). Optional parameters: I= (fade-in), O= (fade-out), X (crossfade), L (loop), V= / VL= / VR= (per-buf volume).

Crossfade (X): if the channel has a head buf already playing, its decay overlaps with the new buf’s fade-in over fade_in_frames frames — both play concurrently during the overlap. No-op if the channel is empty (the new buf just fades in from silence).

Loop (L): the buf’s mixer cursor wraps at end. A subsequent Queue (plain or crossfade) ends the loop.

APC SyncTERM:A;Flush;C=#[;O=<dur>] ST Flush channel (CTAFL)

Drains channel C’s queue. If `O= is provided, applies an overlay fade-out of that length on the current head buf first; otherwise stops immediately.

APC SyncTERM:A;Volume;C=#;…​ ST Set channel volume (CTAV)

Updates the channel’s base dB. V= sets both channels; VL= / VR= override a specific side. If only one side is given, the other is left at its current value.

An optional T=<duration> parameter turns the change into a dB-linear ramp that interpolates from the current volume to the target over <duration> (same grammar as Synth’s `T — milliseconds by default, f suffix for frames, ms for explicit ms; p periods are rejected). T=0 or absent → instant change.

APC SyncTERM:A;Wait;C=# ST Wait for channel to idle (CTAW)

Blocks the terminal (stalling further BBS byte processing) until channel `C’s queue is empty. No-op if the head is a looping buf.

APC SyncTERM:A;Update;C=# ST Arm idle notification (CTAU)

Arms a one-shot CSI = 7 ; <ch> ; 0 n emission the next time channel C transitions from running to stopped. BBS can poll channel state (SyncTERM:A;Queue followed by a state query) without busy-waiting.

CSI = 7 n DSR — audio channel + feature state

Unified response sequence for audio channel state queries AND feature presence queries. The grammar is:

CSI = 7 [ ; <id> ; <state> ]…​ n

— zero or more (id, state) pairs packed into one sequence. id is a channel number (0..15) or a feature ID (100+).

States: 0 = stopped, 1 = running. For feature IDs: 0 = unavailable, 1 = available. Feature IDs defined:

  • 100 — libsndfile

Query forms:

  • CSI = 7 n — all running channels. Emits one pair per running channel; non-running channels omitted. A fully idle terminal replies with just CSI = 7 n (no pairs).

  • CSI = 7 ; <ch> n — one specific channel. Always emits exactly one pair.

Async notification:

  • CSI = 7 ; <ch> ; <state> n — emitted when a channel armed via Update transitions running→stopped. Each notification is a full DSR in its own right, emitted asynchronously from the idle- transition poll.

"ANSI" Music

This is the place where the BBS world completely fell on it’s face in ANSI usage. A programmer with either TeleMate or QModem (the first two programs to support "ANSI" music as far as I can tell) decided they needed a method of playing music on a BBS connection. They decided to add an "unused" ANSI code and go their merry way. Since their product didn’t implement CSI M (Delete line) they assumed it was unused and blissfully broke the spec. They defined "ANSI" music as: CSI M <music string> 0x0e

They used a subset of IBM BASICs PLAY statement functionality for ANSI music strings which often start with "MF" or "MB", so the M after the CSI was often considered as part of the music string. You would see things such as: CSI MFABCD 0x0e and the F would not be played as a note. This just added further confusion to the mess.

Later on, BananaCom realized the conflict between delete line and music, so they added another broken code CSI N (Properly, erase in field…​ not implemented in many BBS clients) which was to provide an "unbroken" method of playing music strings. They also used CSI Y to disambiguate delete line, CSI Y is supposed to be a vertical tab (also not implemented in very many clients). BananaCom also introduced many more non-standard and standard-breaking control sequences which are not supported by CTerm.

CTerm has further introduced a standard compliant ANSI music introducer CSI |

By default, CTerm allows both CSI N and CSI | to introduce a music string. Allowed introducers are set by CSI = p1 M as defined above.

The details of ANSI music then are as follows: The following characters are allowed in music strings: "aAbBcCdDeEfFgGlLmMnNoOpPsStT0123456789.-+#<> " If any character not in this list is present, the music string is ignored as is the introducing code.

If the introducing code is CSI M the first char is examined, and if it is a one of "BbFfLlSs" or if it is "N" or "n" and is not followed by a decimal digit, then the music string is treated as though an M is located in front of the first character.

The music string is then parsed with the following sequences supported:

Mx

sets misc. music parameters where x is one of the following:

F

Plays music in the foreground, waiting for music to complete playing before more characters are processed.

B

Play music in the background, allowing normal processing to continue.

N

"Normal" not legato, not staccato

L

Play notes legato

S

Play notes staccato

T###

Sets the tempo of the music where ### is one or more decimal digits. If the decimal number is greater than 255, it is forced to 255. If it is less than 32, it is forced to 32. The number signifies quarter notes per minute. The default tempo is 120.

O###

Sets the octave of the music where ### is one or more decimal digits. If the decimal number is greater than 6, it is forced to 6. The default octave is 4.

N###

Plays a single note by number. Valid values are 0 - 71. Invalid values are played as silence. Note zero is C in octave 0. See following section for valid note modifiers.

A, B, C, D, E, F, G, or P

Plays the named note or pause from the current octave. An "Octave" is the rising sequence of the following notes: C, C#, D, D#, E, F, F#, G, G#, A, A#, B The special note P is a pause. Notes may be followed by one or more modifier characters which are applied in order. If one overrides a previous one, the last is used. The valid modifiers are:

+ - Sharp

The next highest semitone is played. Each sharp character will move up one semitone, so "C++" is equivalent to "D".

# - Sharp

The next highest semitone is played. Each sharp character will move up one semitone, so "C##" is equivalent to "D".

- - Flat

The next lowest semitone is played. Each flat character will move down one semitone, so "D--" is equivalent to "C".

. - Duration is 1.5 times what it would otherwise be

Dots are not cumulative, so C.. is equivalent to C.

### - Notelength as a reciprocal of the fraction of a whole note to play the note for

For example, 4 would indicate a 1/4 note. The default note length is 4.

L###

Set the notelength parameter for all following notes which do not have one specified (ie: override the quarter-note default) Legal note lengths are 1-64 indicating the reciprocal of the fraction (ie: 4 indicates a 1/4 note).

<

Move the next lowest octave. Octave cannot go above six or below zero.

>

Move to the next highest octave. Octave cannot go above six or below zero.

The lowest playable character is C in octave zero. The frequencies for the six C notes for the seven octaves in rising order are: 65.406, 130.810, 261.620, 523.250, 1046.500, 2093.000, 4186.000

Purists will note that the lower three octaves are not exactly one half of the next higher octave in frequency. This is due to lost resolution of low frequencies. The notes sound correct to me. If anyone can give me an excellent reason to change them (and more correct integer values for all notes) I am willing to do that assuming the notes still sound "right".

NMOTE: If you are playing some ANSI Music then ask the user if they heard it, ALWAYS follow it with an 0x0f 0x0e is the shift lock character which will cause people with anything but an ANSI-BBS terminal (ie: *nix users using the bundled telnet app) to have their screen messed up. 0x0f "undoes" the 0x0e.

Sequences sent by SyncTERM

The following keys in SyncTERM result in the specified sequence being sent to the remote. This is not part of CTerm, but are documented here for people who want to maintain compatibility.

Left Arrow

"\033[D"

Right Arrow

"\033[C"

Up Arrow

"\033[A"

Down Arrow

"\033[B"

Home

"\033[H"

End

"\033[K"

Select

"\033[K" (Same as End due to termcap weirdness)

Backspace

"\b" when DECBKM is set (default), "\x7f" when reset

Delete

"\x7f" when DECBKM is set (default), "\033[3~" when reset

Page Down

"\033[U"

Page Up

"\033[V"

F1

"\033[11~"

F2

"\033[12~"

F3

"\033[13~"

F4

"\033[14~"

F5

"\033[15~"

F6

"\033[17~" (Note the jump from 15 to 17 here)

F7

"\033[18~"

F8

"\033[19~"

F9

"\033[20~"

F10

"\033[21~"

F11

"\033[23~" (Note the jump from 21 to 23 here)

F12

"\033[24~"

Shift + F1

"\033[11;2~"

Shift + F2

"\033[12;2~"

Shift + F3

"\033[13;2~"

Shift + F4

"\033[14;2~"

Shift + F5

"\033[15;2~"

Shift + F6

"\033[17;2~"

Shift + F7

"\033[18;2~"

Shift + F8

"\033[19;2~"

Shift + F9

"\033[20;2~"

Shift + F10

"\033[21;2~"

Shift + F11

"\033[23;2~"

Shift + F12

"\033[24;2~"

Alt + F1

"\033[11;3~"

Alt + F2

"\033[12;3~"

Alt + F3

"\033[13;3~"

Alt + F4

"\033[14;3~"

Alt + F5

"\033[15;3~"

Alt + F6

"\033[17;3~"

Alt + F7

"\033[18;3~"

Alt + F8

"\033[19;3~"

Alt + F9

"\033[20;3~"

Alt + F10

"\033[21;3~"

Alt + F11

"\033[23;3~"

Alt + F12

"\033[24;3~"

Control + F1

"\033[11;5~"

Control + F2

"\033[12;5~"

Control + F3

"\033[13;5~"

Control + F4

"\033[14;5~"

Control + F5

"\033[15;5~"

Control + F6

"\033[17;5~"

Control + F7

"\033[18;5~"

Control + F8

"\033[19;5~"

Control + F9

"\033[20;5~"

Control + F10

"\033[21;5~"

Control + F11

"\033[23;5~"

Control + F12

"\033[24;5~"

Insert

"\033[@"

Back Tab

"\033[Z"

Prestel and BBC Micro (Mode 7) Emulation

CTerm implements two related Videotex/Teletext display modes: Prestel (the UK viewdata terminal standard) and BBC Micro Mode 7 (the BBC Microcomputer’s teletext display mode). Both use the same 40×25 character grid with the same mosaic graphics and serial attribute system, but differ in their C0 control character handling and cursor movement behavior.

Both modes use the PRESTEL_40X25 video mode: 40 columns × 25 rows, 12×20 character cells, with the Prestel 8-color palette (black, red, green, yellow, blue, magenta, cyan, white). Bright backgrounds and no-blink are NOT enabled in this mode (unlike the Commodore and Atari modes).

Serial Attributes

Unlike conventional terminals where attributes are invisible state changes, Prestel/Mode 7 uses serial attributes. Control codes in the range 0x40–0x5F (accessed via ESC + character in Prestel mode, or raw bytes 0x80–0x9F as C1 controls, or via ESC in BEEB mode) occupy a character cell position on screen. The control code cell is displayed as either a space or a held mosaic character.

Serial attributes apply to all subsequent cells on the same row until changed by another control code. At the start of each new row, all attributes reset to their defaults: white alphanumeric text, black background, no flash, no conceal, no hold, contiguous mosaics, normal height.

Control code effects are split into before and after phases:

  • Before effects apply to the control code’s own cell position (steady, normal height, conceal, contiguous/separated mosaics, black background, new background, hold mosaics)

  • After effects apply to cells following the control code (alphanumeric colors, mosaic colors, flash, double height, release mosaics)

Serial Attribute Codes

These are the decoded control values (0x40–0x5F range after ESC, or 0x80–0x9F as raw C1 bytes minus 0x40):

Table 14. After-effect controls
Code Name Action

0x41

Alpha Red

Set foreground to red, switch to alphanumeric mode

0x42

Alpha Green

Set foreground to green, switch to alphanumeric mode

0x43

Alpha Yellow

Set foreground to yellow, switch to alphanumeric mode

0x44

Alpha Blue

Set foreground to blue, switch to alphanumeric mode

0x45

Alpha Magenta

Set foreground to magenta, switch to alphanumeric mode

0x46

Alpha Cyan

Set foreground to cyan, switch to alphanumeric mode

0x47

Alpha White

Set foreground to white, switch to alphanumeric mode

0x48

Flash

Enable flashing (blink attribute)

0x4D

Double Height

Enable double-height for this row (see below)

0x51

Mosaic Red

Set foreground to red, switch to mosaic mode

0x52

Mosaic Green

Set foreground to green, switch to mosaic mode

0x53

Mosaic Yellow

Set foreground to yellow, switch to mosaic mode

0x54

Mosaic Blue

Set foreground to blue, switch to mosaic mode

0x55

Mosaic Magenta

Set foreground to magenta, switch to mosaic mode

0x56

Mosaic Cyan

Set foreground to cyan, switch to mosaic mode

0x57

Mosaic White

Set foreground to white, switch to mosaic mode

0x5F

Release Mosaics

Clear held mosaic, disable hold mode

Table 15. Before-effect controls
Code Name Action

0x49

Steady

Disable flashing (clear blink attribute)

0x4C

Normal Height

Disable double-height, clear hold and held mosaic

0x58

Conceal

Enable concealed display (hidden until revealed)

0x59

Contiguous Mosaics

Switch to contiguous (solid) mosaic rendering

0x5A

Separated Mosaics

Switch to separated (gapped) mosaic rendering

0x5C

Black Background

Set background to black

0x5D

New Background

Set background to current foreground color

0x5E

Hold Mosaics

Enable hold mode (see below)

Alphanumeric vs Mosaic Mode

The alphanumeric color codes (0x41–0x47) switch to the G0 character set where bytes 0x20–0x7F display as normal text characters.

The mosaic color codes (0x51–0x57) switch to the G1 character set. In mosaic mode, characters in the ranges 0x20–0x3F and 0x60–0x7F are displayed as 2×3 block mosaic graphics (with bit 7 set in the stored character value). Characters 0x40–0x5F remain alphanumeric even in mosaic mode.

Hold Mosaics

When a mosaic color change occurs, the control code cell would normally display as a space. With hold mosaics enabled (code 0x5E), the control code cell instead displays the last mosaic character that was output, preserving visual continuity across color changes.

Hold mode is cleared by: alphanumeric color codes, normal height, double height, and release mosaics.

Double Height

Double-height mode (code 0x4D) causes characters on the current row to be rendered at twice their normal height, spanning two physical screen rows. The top half is rendered on the row containing the double-height code, and the bottom half on the row below.

Double-height tracking is complex:

  • A row containing any double-height code becomes a "top" row

  • The row immediately below a "top" row becomes a "bottom" row

  • Bottom rows in Prestel terminal mode copy attributes and characters from the corresponding top row cell

  • Bottom rows in BEEB mode display the bottom half of the same cell

  • The last screen row cannot be a top row

  • Double-height codes also clear hold mode and the held mosaic

Concealed Display

Code 0x58 enables concealed mode. Concealed characters are stored normally but rendered with the foreground hidden (displayed as background color). The user can toggle reveal mode (in SyncTERM, via Alt-V) to show concealed content.

Concealment is cleared by any alphanumeric or mosaic color code.

Character Translation (BEEB only)

In BBC Micro mode, three characters are translated to match the Mode 7 SAA5050 character set:

  • # (0x23) becomes _ (0x5F) — pound sign position

  • _ (0x5F) becomes ` (0x60)

  • ` (0x60) becomes # (0x23)

C0 Control Characters — Prestel
Byte Name Action

0x00

NUL

Time filling — no action (flushes print buffer)

0x05

ENQ

Send memory slot 0 contents (Prestel identity)

0x08

APB

Active Position Backward — move cursor left one position (wraps to end of previous row; top row wraps to bottom)

0x09

APF

Active Position Forward — move cursor right one position (wraps to start of next row; bottom row wraps to top)

0x0A

APD

Active Position Down — move cursor down one row (wraps to top row)

0x0B

APU

Active Position Up — move cursor up one row (wraps to bottom row)

0x0C

CS

Clear Screen — home cursor and clear page memory, reset reveal mode

0x0D

APR

Active Position Return — move cursor to first position of current row

0x11

CON

Cursor On — make cursor visible

0x14

COF

Cursor Off — make cursor invisible

0x1B

ESC

Escape — prefix for serial attribute codes and programming sequences

0x1E

APH

Active Position Home — move cursor to first position of top row

Per the specification, "for all cursor movements the first character of each row is regarded as contiguous with the last character of the previous row, and the top row is regarded as the row following the bottom row." This means all cursor movement wraps around the screen in Prestel mode.

Bytes 0x20–0x7F are printable (with mosaic translation as described above). Bytes 0x80–0x9F are treated as raw C1 serial attribute controls (value minus 0x40). Other undefined C0 bytes are ignored (section 2.6: "shall take no action on their receipt").

C0 Control Characters — BEEB (BBC Micro Mode 7)
Byte Name Action

0x00

NUL

No-op (flushes print buffer)

0x07

BEL

Audible bell

0x08

APB

Active Position Backward — move cursor left (wraps to previous row; scrolls down at top)

0x09

APF

Active Position Forward — move cursor right (wraps to next row; scrolls up at bottom)

0x0A

APD

Active Position Down — move cursor down one row (scrolls up at bottom)

0x0B

APU

Active Position Up — move cursor up one row (scrolls down at top)

0x0C

CS

Clear Screen — home cursor and clear screen, reset reveal mode

0x0D

APR

Active Position Return — move cursor to first position of current row (via ctputs CR handling)

0x17

VDU 23

Start 9-byte VDU 23 sequence (cursor control only)

0x1C

APS

Active Position Set — followed by 2 bytes: column (minus 0x20), row (minus 0x20), both 0-based

0x1E

APH

Active Position Home — move cursor to first position of top row

0x7F

DEL

Destructive backspace — move left, write space, move left

Note
LF (0x0A) in BEEB mode is handled by the explicit APD case before reaching ctputs, so it functions as cursor-down with scroll, not as a ctputs line feed.

Unlike Prestel, BEEB mode scrolls the screen when cursor movement reaches the edges rather than wrapping around.

Cursor Movement Differences

The key behavioral difference between Prestel and BEEB modes is cursor wrapping at screen edges:

  • Prestel: Cursor wraps around the screen. Moving up from the top row wraps to the bottom row. Moving down from the bottom row wraps to the top row.

  • BEEB: Cursor scrolls the screen. Moving up from the top row scrolls content down. Moving down from the bottom row scrolls content up.

Left/right wrapping to adjacent rows is the same in both modes.

ESC Sequences — Prestel

In Prestel mode, ESC is followed by a character that is interpreted as a serial attribute code (the character value is used directly as the control code, range 0x40–0x5F for valid codes).

Additionally, Prestel supports a programming protocol for memory slot management:

  • ESC 1 ESC 2 — Begin memory query/program sequence

  • ENQ (0x05) within programming — send current memory slot

  • ESC 3 — advance to next memory slot

  • ESC 4 — begin programming current memory slot (followed by 16 data bytes using digits 0–9, :, ;, ?)

  • 7 memory slots of 16 bytes each are available

ESC/VDU Sequences — BEEB

In BEEB mode, ESC does NOT trigger serial attribute codes (unlike Prestel). Serial attributes in BEEB mode are delivered as raw C1 bytes (0x80–0x9F) which are translated to attribute codes by subtracting 0x40.

ESC in BEEB mode introduces VDU control sequences. Two multi-byte sequences are supported:

  • VDU 23,1,N;0;0;0; (byte 23 + 9 data bytes): Cursor control. N=0 hides cursor, N=1 shows cursor. All other bytes in the sequence must be zero.

  • APS (byte 28 + 2 bytes): Direct cursor addressing. First byte is column (minus 0x20), second is row (minus 0x20), both 0-based.

Bitmap Rendering

Prestel/Mode 7 rendering is handled specially in the bitmap driver (bitmap_con.c) because it requires features not available in the standard text rendering path:

  • Separated mosaics: Mosaic characters are rendered with gaps between the 2×3 blocks, using the CIOLIB_BG_SEPARATED flag

  • Double-height rendering: Characters are stretched vertically across two physical rows, with top/bottom half tracking per row

  • Concealed display: The CIOLIB_BG_PRESTEL flag enables special concealment rendering where foreground pixels are suppressed unless CONIO_OPT_PRESTEL_REVEAL is set

  • Row state caching: Double-height row states (top/bottom) are pre-computed to avoid O(rows²) scanning during rendering

The CIOLIB_BG_PRESTEL_TERMINAL flag distinguishes Prestel terminal mode from BEEB mode for double-height bottom-row handling: Prestel copies attributes from the row above, while BEEB uses the cell’s own attributes.

PETSCII Emulation

CTerm’s PETSCII mode emulates the Commodore 64 and Commodore 128 screen editors. PETSCII (PET Standard Code of Information Interchange) is the character encoding used by Commodore 8-bit computers, with control codes for color, cursor movement, and screen editing.

Three screen modes are supported:

Mode Size Colors Default Attr

C64 40×25

40×25

16 (C64 palette)

0x6E (light blue on blue)

C128 40×25

40×25

16 (C64 palette)

0xBD (light green on dark green)

C128 80×25

80×25

16 (CGA palette)

0x07 (light grey on black)

All three modes have bright backgrounds enabled and blinking disabled (BGBRIGHT and NOBLINK video flags).

Each mode has two font variants: upper-case/graphics (the default) and lower-case/upper-case (selected by control codes 14 and 142).

Control Codes
Byte Name Action

5

White

Set foreground color to white

7

Bell

Audible bell

13

Return

Carriage return + line feed. Also disables reverse mode. Scrolls at bottom.

14

Lower Case

Switch to lower-case/upper-case font

17

Cursor Down

Move cursor down one row (scrolls at bottom)

18

Reverse On

Enable reverse video mode

19

Home

Move cursor to top-left corner

20

Delete

Destructive backspace — move cursor left (wrapping to end of previous row), shift remaining characters left, blank inserted at right margin. At top-left, does nothing.

28

Red

Set foreground color to red

29

Cursor Right

Move cursor right (wraps to first column of next row, scrolls at bottom)

30

Green

Set foreground color to green

31

Blue

Set foreground color to blue

129

Orange

Set foreground color to orange (C64/C128-40) or magenta (C128-80)

141

Shift+Return

Line feed (move to first column of next row, scrolls at bottom). Does NOT disable reverse mode.

142

Upper Case

Switch to upper-case/graphics font

144

Black

Set foreground color to black

145

Cursor Up

Move cursor up one row (clamps at top, no scroll)

146

Reverse Off

Disable reverse video mode

147

Clear Screen

Clear screen and home cursor

148

Insert

Insert a blank space at cursor; characters to the right shift right. Character at right margin is lost.

149

Brown

Set foreground color

150

Light Red

Set foreground color

151

Dark Grey

Set foreground color

152

Grey

Set foreground color

153

Light Green

Set foreground color

154

Light Blue

Set foreground color

155

Light Grey

Set foreground color

156

Purple

Set foreground color

157

Cursor Left

Move cursor left (wraps to last column of previous row if at left margin; clamps at top-left)

158

Yellow

Set foreground color

159

Cyan

Set foreground color

Bytes 32–127 and 160–255 are printable characters (unless listed above). Bytes in ranges 0–31 and 128–159 that are not listed above are ignored.

Known Differences from Hardware

The Commodore 128 KERNAL handles several control codes differently from the C64. CTerm currently uses C64 behavior for most of these. The following codes differ between C64 and C128 hardware but are not yet differentiated in CTerm:

Byte C64 Hardware C128 Hardware

0x02

Ignored

UL ON (underline, C128 only)

0x08

LOCK CASE

TAB SET/CLEAR (HTS)

0x09

UNLOCK CASE

TAB (HT)

0x0A

Ignored

LINE FEED

0x0B

Ignored

UNLOCK CASE

0x0C

Ignored

LOCK CASE

0x0F

Ignored

FSH ON (flashing, 80-column only)

0x1B

Ignored

ESC

0x82

Ignored

UL OFF (underline off, C128 only)

0x8F

Ignored

FSH OFF (flashing off, 80-column only)

Note
The actual behavior desired may differ from raw hardware because BBS terminal programs (CCGMS, DesTerm, NovaTerm, etc.) running on these systems may have performed their own translation or stripping of control codes before displaying them.
Color Mapping

The 16 color control codes map to different palette indices depending on the screen mode.

Table 16. C64 and C128 40-column color mapping
Byte Color Name Index Byte Color Name Index

144

Black

0

129

Orange

8

5

White

1

149

Brown

9

28

Red

2

150

Light Red

10

159

Cyan

3

151

Dark Grey

11

156

Purple

4

152

Grey

12

30

Green

5

153

Light Green

13

31

Blue

6

154

Light Blue

14

158

Yellow

7

155

Light Grey

15

Table 17. C128 80-column color mapping
Byte Color Name Index Byte Color Name Index

144

Black

0

152

Grey

8

31

Blue

1

154

Light Blue

9

30

Green

2

153

Light Green

10

151

Dark Grey

3

159

Cyan

11

28

Red

4

150

Light Red

12

129

Orange

5

156

Purple

13

149

Brown

6

158

Yellow

14

155

Light Grey

7

5

White

15

Note
The C64 and C128 40-column modes use the C64 VIC-II palette. The C128 80-column mode uses a standard CGA-compatible palette. The same control code byte may map to a different palette index depending on the mode.
Reverse Video

Reverse video mode (byte 18 on, byte 146 off) swaps the foreground and background nibbles of the attribute byte for all subsequent characters. Byte 13 (Return) also disables reverse mode.

Cursor Movement Details
  • Return (13): Moves cursor to first column and down one row. Scrolls if at bottom. Also disables reverse mode.

  • Shift+Return (141): Same as Return but does NOT disable reverse.

  • Cursor Down (17): Move down one row, scrolls at bottom.

  • Cursor Up (145): Move up one row, clamps at top (no scroll).

  • Cursor Right (29): Move right one column. At right margin, wraps to first column of next row. At bottom-right, scrolls.

  • Cursor Left (157): Move left one column. At left margin, wraps to last column of previous row. At top-left, does nothing.

Font Switching

Two character sets are available per machine type:

Mode Upper Case (142) Lower Case (14)

C64

Font 32 (C64 UPPER)

Font 33 (C64 Lower)

C128

Font 34 (C128 UPPER)

Font 35 (C128 Lower)

The upper-case set contains upper-case letters and PETSCII graphics characters. The lower-case set contains both upper and lower-case letters.

Unimplemented Features
  • Flashing on/off (bytes 2/0x82 and 15/0x8F) — C128 80-column only, not implemented

ATASCII Emulation

CTerm’s ATASCII mode emulates the Atari 8-bit computer’s screen editor. ATASCII (Atari ASCII) is the character encoding used by Atari 400/800 and XL/XE series computers, with control codes and cursor behavior specific to the Atari hardware.

Two screen modes are supported:

Mode Size Colors Notes

Standard (Atari 40×24)

40×24

2

Dark blue background, light blue foreground

XEP80 (Atari 80×25)

80×25

2

Greyscale (white and black)

No escape sequences are used; all operations are performed via single-byte control codes.

Character Encoding

In normal mode, printable bytes are stored directly as raw ATASCII values in the screen buffer. The font rendering maps these to the appropriate Atari glyphs.

In inverse (ESC) mode, bytes are translated to Atari screen codes before storage:

  • Bytes 0–31 are mapped to screen codes 64–95

  • Bytes 32–95 are mapped to screen codes 0–63

  • Bytes 96–127 are not translated

  • Bytes 128–159 are mapped to screen codes 192–223

  • Bytes 160–223 are mapped to screen codes 128–191

  • Bytes 224–255 are not translated

ESC Mode

Byte 27 (ESC) enables inverse video mode. The next byte received is translated to a screen code (see above) and displayed using the inverse attribute (attr=1 instead of the normal attr=7). After displaying the inverse character, normal mode is automatically restored. The ESC byte itself is not displayed.

In ESC mode, byte 155 (Return) still functions as a control code. All other bytes are translated to screen codes and displayed in inverse.

Control Codes
Byte Name Action

27

ESC

Enable inverse video for the next character

28

Cursor Up

Move cursor up one row; wraps to bottom of same column

29

Cursor Down

Move cursor down one row; wraps to top of same column

30

Cursor Left

Move cursor left one column; wraps to right side of same row

31

Cursor Right

Move cursor right one column; wraps to left side of same row

125

Clear Screen

Clear entire screen and home cursor

126

Backspace

Move cursor left one column and erase the character at the new position. Does NOT wrap; sticks at left margin.

127

Tab

Advance to next tab stop. If no tab stop is found before end of line, wraps to first column of next row (scrolling if at bottom).

155

Return (EOL)

Move cursor to first column of next row (CR+LF). Scrolls if at bottom.

156

Delete Line

Delete the current line; lines below shift up. Cursor moves to first column.

157

Insert Line

Insert a blank line at cursor row; lines below shift down. Current line is cleared.

158

Clear Tab

Clear the tab stop at the current cursor column

159

Set Tab

Set a tab stop at the current cursor column

253

Bell

Audible bell

254

Delete Char

Delete the character at the cursor; characters to the right shift left. A blank is inserted at the right margin.

255

Insert Char

Insert a blank space at the cursor; characters to the right shift right. The character at the right margin is lost.

All other byte values (including 0–26 except ESC, 32–124, 128–154, 160–252) are treated as printable characters and displayed using the ATASCII-to-screen-code translation described above.

Cursor Wrapping Behavior

The four cursor movement keys wrap differently than most terminals:

  • Up (28): Wraps from the top row to the bottom row of the same column. Does NOT scroll.

  • Down (29): Wraps from the bottom row to the top row of the same column. Does NOT scroll.

  • Left (30): Wraps from the left margin to the right margin of the same row. Does NOT change rows.

  • Right (31): Wraps from the right margin to the left margin of the same row. Does NOT change rows.

This matches verified Atari 8-bit hardware behavior.

Tab Stops

Tab stops in ATASCII are set and cleared at individual column positions using bytes 159 (Set Tab) and 158 (Clear Tab). Default tab stops are at the standard 8-column intervals. The tab character (byte 127) advances to the next set tab stop; if no stop exists before the end of the line, it wraps to column 1 of the next row (scrolling if necessary).

Atari ST VT52 Emulation

CTerm’s Atari ST VT52 mode emulates the VT52-compatible terminal built into the Atari ST’s TOS/GEM desktop. This includes the standard VT52 command set plus GEMDOS/TOS extensions for color, line editing, and additional cursor control.

Three screen modes are supported, matching the Atari ST’s display hardware:

Mode Size Colors Notes

Low resolution (ST 40×25)

40×25

16

Full 16-color palette

Medium resolution (ST 80×25)

80×25

4

White, Red, Green, Black

High resolution (ST 80×25 Mono)

80×25

2

White and Black

Autowrap is disabled by default (unlike ANSI-BBS mode). All three modes have bright backgrounds enabled and blinking disabled (BGBRIGHT and NOBLINK video flags).

The available colors depend on the screen mode — color commands (ESC b, ESC c) index into the mode’s palette, and indices beyond the mode’s color count wrap around (only the low bits are meaningful).

C0 Control Characters
Byte Name Action

0x00

NUL

Ignored

0x01–0x06

Ignored

0x07

BEL

Audible bell

0x08

BS

Backspace — move cursor left one column (no wrap, clamps at left margin)

0x09

HT

Horizontal tab — advance to next tab stop

0x0A

LF

Line feed — move cursor down one row, scroll if at bottom

0x0B

VT

Vertical tab — same as LF (move down one row, scroll)

0x0C

FF

Form feed — same as LF (move down one row, scroll)

0x0D

CR

Carriage return — move cursor to left margin

0x0E–0x1A

Ignored

0x1B

ESC

Start escape sequence

0x1C–0x1F

Ignored

0x20–0x7E

Printable character, displayed at cursor position

BS, HT, LF, and CR are processed by the shared ctputs output path. VT and FF are handled explicitly before output and behave identically to LF (move down one row with scroll).

Escape Sequences — Standard VT52

All sequences are two bytes: ESC followed by a single character, except ESC Y which takes two additional parameter bytes.

Sequence Name Action

ESC A

Cursor Up

Move cursor up one row (clamp at top, no scroll)

ESC B

Cursor Down

Move cursor down one row (clamp at bottom, no scroll)

ESC C

Cursor Right

Move cursor right one column (clamp at right margin)

ESC D

Cursor Left

Move cursor left one column (clamp at left margin)

ESC H

Cursor Home

Move cursor to top-left corner (row 1, column 1)

ESC I

Reverse Line Feed

Move cursor up one row; if at top, scroll down

ESC J

Erase to End of Page

Clear from cursor to end of line, then clear all lines below

ESC K

Erase to End of Line

Clear from cursor to end of current line

ESC Y row col

Direct Cursor Address

Move cursor to position (row − 32, col − 32). Row and column are 0-based offsets encoded as the value + 32 (space = 0). Row is clamped to 0–23, column to 0–79. Parameters below 32 abort the sequence.

ESC ESC

Ignored (ESC followed by ESC)

ESC =

Alternate Keypad

Enable alternate keypad mode

ESC >

Normal Keypad

Disable alternate keypad mode (normal numeric keypad)

ESC F

Ignored (VT52 graphics mode — not applicable)

ESC G

Ignored (VT52 graphics mode — not applicable)

ESC [

Ignored (hold-screen mode — not implemented)

ESC \

Ignored (hold-screen mode — not implemented)

Escape Sequences — GEMDOS/TOS Extensions

These are extensions specific to the Atari ST’s TOS operating system.

Sequence Name Action

ESC E

Clear Screen

Clear entire screen and move cursor to home position

ESC L

Insert Line

Insert a blank line at cursor row; lines below shift down. Cursor column is preserved.

ESC M

Delete Line

Delete the line at cursor row; lines below shift up. A blank line is added at the bottom.

ESC b c

Set Foreground Color

Set foreground color to c & 0x0F (4-bit palette index, 0–15)

ESC c c

Set Background Color

Set background color to c & 0x0F (4-bit palette index, 0–15)

ESC d

Erase to Start of Page

Clear from cursor to start of line (inclusive), then clear all lines above

ESC e

Show Cursor

Make cursor visible (normal cursor)

ESC f

Hide Cursor

Make cursor invisible

ESC j

Save Cursor

Save current cursor position

ESC k

Restore Cursor

Restore cursor to last saved position. No effect if no position was saved or if saved position is outside the current window.

ESC l

Clear Line

Clear the entire current line (cursor position is preserved)

ESC o

Erase to Start of Line

Clear from start of line to cursor position (inclusive)

ESC p

Reverse Video On

Enable reverse video (swap foreground and background)

ESC q

Reverse Video Off

Disable reverse video (restore normal foreground/background)

ESC v

Enable Autowrap

Characters at the right margin wrap to the next line

ESC w

Disable Autowrap

Characters at the right margin are clamped (no wrap)

Color Palette

The color parameter for ESC b and ESC c is the low 4 bits of the byte following the command character (value & 0x0F), giving an index from 0 to 15 into the current mode’s palette.

In low resolution (40×25), the full 16-color palette is available:

Index Color Index Color

0

White

8

Light Red

1

Red

9

Light Green

2

Green

10

Light Yellow

3

Yellow

11

Light Blue

4

Blue

12

Light Magenta

5

Magenta

13

Light Cyan

6

Cyan

14

Dark Grey

7

Light Grey

15

Black

In medium resolution (80×25), the default palette has 4 colors repeated across all 16 slots: White (0), Red (1), Green (2), Black (3), matching the Atari ST hardware where only 4 simultaneous colors were available.

In high resolution (80×25 mono), the default palette alternates White and Black across all 16 slots, matching the monochrome hardware.

Note
SyncTERM’s per-entry custom palette feature allows users to override the default palette with up to 16 unique colors in any mode. This does not match hardware behavior where medium and high resolution modes are physically limited to 4 and 2 colors respectively, but provides flexibility for BBS content that uses the additional palette slots.
Note
Reverse video (ESC p / ESC q) interacts with color commands. When reverse video is active, ESC b sets the background color and ESC c sets the foreground color (the sense is swapped).
Negative Image (Reverse Video)

The Atari ST VT52 mode uses the full 4-bit foreground and background nibbles when swapping for reverse video, unlike ANSI-BBS mode which only swaps the low 3 bits and preserves the bright/blink bits independently. In VT52 mode, ESC p swaps all 4 bits of the foreground nibble with all 4 bits of the background nibble.

Differences from Standard VT52
  • Autowrap is off by default (standard VT52 has no autowrap control)

  • 16-color support via ESC b and ESC c (Atari ST extension)

  • Insert/delete line (ESC L / ESC M) are TOS extensions

  • Erase-to-start commands (ESC d, ESC o) are TOS extensions

  • Cursor save/restore (ESC j / ESC k) are TOS extensions

  • Show/hide cursor (ESC e / ESC f) are TOS extensions

  • Autowrap control (ESC v / ESC w) are TOS extensions

  • Reverse video (ESC p / ESC q) are TOS extensions

  • Clear line (ESC l) is a TOS extension

  • VT52 Identify (ESC Z) is not implemented

  • VT52 graphics mode (ESC F / ESC G) is not implemented

  • Hold-screen mode (ESC [ / ESC \) is not implemented

References

  • [STD-070] Digital Equipment Corporation. Video Systems Reference Manual. 1989-04-14.

  • [ECMA-48] ECMA. Control Functions for Coded Character Sets. June 1991

  • [XTerm] Edward May. XTerm Control Sequences. University of California, Berkeley. 2024/09/19

  • [Paste64] Thomas E. Dickey. XTerm — bracketed paste. 2022

  • [BANSI] Paul Wheaton. BANSI.TXT. 1999

  • [VT102] Digital. VT102 Video Terminal User Guide. 1982.

  • [VT330/340] Digital. VT330/VT340 Programmer Reference Manual, Volume 2: Graphics Programming. May 1988.

  • [VT320] Digital. Installing and Using the VT320 Video Terminal. June 1987.

  • [256colors] Jonas Jarad Jacek. 256 colors cheat sheet. 2023-12-24.

  • [VT420] Digital. Installing and Using the VT420 Video Terminal. June 1990.

  • [ANSISYS] Wikipedia. ANSI.SYS.

  • [ECMA-35] ECMA. Character Code Structure and Extension Techniques. December 1994

  • [VT520] Digital. VT520/VT525 Video Terminal Programmer Information. July 1994.

  • [VT100] Digital. User Guide VT100. 1981.

Ciolib Manual

Introduction

Ciolib originated as a FreeBSD/Linux implementation of the Borland conio library for use by the Synchronet User InterFaCe library (UIFC). Since then, it has grown more complete by being used to port other software that was originally implemented using Borland C.

The use of ciolib in SyncTERM however, kicked off an explosion in capabilities, and SyncTERM has been the primary driver of ciolib development ever since. Graphics, multiple font, TruColor, window scaling and more have been added to ciolib to extend SyncTERM. In addition, the ANSI parsing and displaying code CTerm is part of ciolib, and not SyncTERM.

Output Modes

An output mode specifies the manner in which ciolib displays the content to the user. There are thirteen ciolib output modes that can be broadly grouped into two categories:

Text Output Modes

These modes use a text based library or interface to display character cells and attributes. These modes are incapable of graphics.

Curses Modes

In one of the curses modes, the curses (or ncurses if available) API provided by the OS is used for output. This means it requires the TERM variable be set appropriately, and needs to run inside of another terminal emulator such as XTerm or Kitty. There are three curses modes.

Curses

This uses the wide char curses API and supports unicode input and output, translating as needed. This provides the highest quality in almost all terminals.

Curses IBM

When in this mode, ciolib assumes that any characters displayed on the screen will be in IBM codepage 437, and translates to and from that as appropriate. This mode can work well inside of a CP437 BBS connection, but makes many assumptions that are suspect.

Curses ASCII

In this mode, output is restricted to the ASCII character set, and everything is translated to that. This is the least capable curses mode.

ANSI Mode

Ciolib will output ANSI control sequences on stdout in this mode. In general, the sequences used are in line with "ANSI-BBS". This is similar to curses mode, except the TERM variable has no impact, and all characteristics of the terminal emulator used for display are simply assumed. This is the best mode to use from inside a BBS connection.

Windows Conio

Only available on Windows, this uses the old Windows NT console API to output text. Newer versions of Windows have changed this API considerably, so until ciolib is updated to support the new console, this is of limited usefulness.

Graphical Modes

In graphical modes, ciolib controls every pixel that is displayed. As a result of this, every feature of ciolib is available to every graphical mode. The main reasons to choose one over another is portability and OS.

Graphical modes usually also have a fullscreen variant.

SDL Mode

SDL mode is the most portable of the modes as it uses libsdl, a library designed for writing cross-platform games. SDL supports many more platforms than ciolib does, and is usually the first graphical mode supported on a new platform.

Unfortunately, SDL mode tends to be more complex and usually somewhat slower or more CPU intensive than other modes, so is usually only used as a fallback.

X mode

This uses libX11 to communicate with an X server. Historically, the GUI for most \*nix systems were provided via an X server. While Linux distributions are moving to Wayland, even Wayland still supports libX11 applications via XWayland.

Wayland mode

This is a native Wayland backend that communicates directly with a Wayland compositor. It uses runtime dynamic loading (dlopen) for libwayland-client, libwayland-cursor, and libxkbcommon, so no link-time dependencies are introduced. If the compositor does not support optional protocols such as clipboard or server-side decorations, those features degrade gracefully. When server-side decorations are not available, holding Alt and dragging with the left mouse button can be used to move the window.

Quartz mode

This is the native macOS backend, using AppKit for window management and Core Graphics for rendering. It is the default on macOS. Audio uses CoreAudio via the AudioQueue API. No SDL dependency is required. Supports internal scaling (xBR) with 1:1 backing pixel mapping on Retina displays, and external scaling via Core Graphics interpolation.

GDI mode

This directly uses the Win32 Graphics Display Interface (GDI) API.

Text Modes

Ciolib operates in exactly one text mode. This is distinct from the Text Output Mode mentioned above. Internally, a text mode is defined via fourteen values:

Mode Number

A unique ID for each mode. Many of these were defined by Borland, but the list has been extended by various parties over the years.

Palette

The palette is a mapping of attribute values to TruColor RGB values. For historical DOS modes, this defines the standard sixteen colours (or up to three intensities for monchrome modes). For other modes such as the Commodore 64 and Atari modes, the palette is very different.

Columns

The number of columns on the display.

Rows

The number of rows on the display.

Cursor Start

The pixel row number the default cursor starts on. DOS modes tend to use a two pixel high underline cursor, while Commodore uses a full block cursor.

Cursor End

The pixel row number the default cursor ends on.

Character Height

The height of a single cell on the display. This indirectly sets the allowed fonts, since only fonts with the specified height can be used.

Character Width

The width of a character cell. In all except one mode, this is 8. However, in the VGA80X25 mode, this is 9. When this is 9, an 8 pixel wide font may still be used if the VIDMODES_FLAG_EXPAND flag is set.

Default Attribute

The attribute used when a screen is initialized. Light Gray on Black for DOS modes, but varies for other modes.

Flags

Flags can be set to control on/off behaviours in a mode. There are currently two flags that can be set

CIOLIB_VIDEO_EXPAND

This flag is used to add an extra pixel to the right side of each character cell that is not present in the font data.

CIOLIB_VIDEO_LINE_GRAPHICS_EXPAND

An algorithm from IBM graphics cards is used to fill in an extra pixel column. This makes line drawing characters connect across the space, but leaves a single pixel gap between block drawing characters.

Aspect Ratio Width

See next item.

Aspect Ratio Height

Aspect Ratio Height and Aspect Ratio Width controls the aspect ratio the mode is scaled to. Most historical text modes did not use square pixels, but ciolib assumes that its output does use square pixels. It’s simplest to describe these old modes using the aspect ratio of the display and the pixel resolution. Historical display were almost universally 4:3 aspect ratio. The Commodore 64 however used large borders on the sides, and the aspect ratio is actually 6:5. There is a small number of additional modes that use aspect ratios with square pixels. These are:

ST132X37_16_9

This is a 132x37 mode with square pixels and a 16:9 aspect ratio.

ST132X37_5_4

This is a 132x52 mode with square pixels and a 5:4 aspect ratio.

LCD80X25

This is an 80X25 mode with square pixels and an 8:5 aspect ratio. This mode was added to provide a way of avoiding scaling and the resulting "blurriness".

X Resolution

The width in pixels of the display.

Y Resolution

The height in pixels of the display.

There is a custom mode defined where the values can be modified by the program to create exactly the desired mode.

Fonts

There are a large number of built in fonts that ciolib supports. Most are codepage fonts, but there is also Commodore, Atari, Amiga, and "Prestel" (SAA5050 as used in BBC Micro Model 7 and others) fonts included in the default set. Not all builtin fonts are available in every size. The following table summarizes the available fonts.

Name 8x16 8x12 8x8 12x20 Character Set

Codepage 437 English

CP437

Codepage 1251 Cyrillic, (swiss)

CP1251

Russian koi8-r

KOI8_R

ISO-8859-2 Central European

ISO_8859_2

ISO-8859-4 Baltic wide (VGA 9bit mapped)

ISO_8859_4

Codepage 866 (c) Russian

CP866M

ISO-8859-9 Turkish

ISO_8859_9

haik8 codepage (use only with armscii8 screenmap)

HAIK8

ISO-8859-8 Hebrew

ISO_8859_8

Ukrainian font koi8-u

KOI8_U

ISO-8859-15 West European, (thin)

ISO_8859_15

ISO-8859-4 Baltic (VGA 9bit mapped)

ISO_8859_4

Russian koi8-r (b)

KOI8_R

ISO-8859-4 Baltic wide

ISO_8859_4

ISO-8859-5 Cyrillic

ISO_8859_5

ARMSCII-8 Character set

ARMSCII8

ISO-8859-15 West European

ISO_8859_15

Codepage 850 Multilingual Latin I, (thin)

CP850

Codepage 850 Multilingual Latin I

CP850

Codepage 865 Norwegian, (thin)

CP865

Codepage 1251 Cyrillic

CP1251

ISO-8859-7 Greek

ISO_8859_7

Russian koi8-r (c)

KOI8_R

ISO-8859-4 Baltic

ISO_8859_4

ISO-8859-1 West European

ISO_8859_1

Codepage 866 Russian

CP866M2

Codepage 437 English, (thin)

CP437

Codepage 866 (b) Russian

CP866M2

Codepage 865 Norwegian

CP865

Ukrainian font cp866u

CP866U

ISO-8859-1 West European, (thin)

ISO_8859_1

Codepage 1131 Belarusian, (swiss)

CP1131

Commodore 64 (UPPER)

PETSCIIU

Commodore 64 (Lower)

PETSCIIL

Commodore 128 (UPPER)

PETSCIIU

Commodore 128 (Lower)

PETSCIIL

Atari

ATASCII

P0T NOoDLE (Amiga)

ISO_8859_1

mO’sOul (Amiga)

ISO_8859_1

MicroKnight Plus (Amiga)

ISO_8859_1

Topaz Plus (Amiga)

ISO_8859_1

MicroKnight (Amiga)

ISO_8859_1

Topaz (Amiga)

ISO_8859_1

Prestel

SAA5050

Atari ST

Atari ST

RIPterm

CP437