Skip to content

Terminal Session and Raw Mode

This page covers the OS-facing layer that puts the terminal into the right state for a full-screen application and reliably restores it afterward: raw mode, the window-size query, and the with-terminal-session lifecycle helper.

SBCL only

Raw mode, terminal-size, and with-terminal-session's raw-mode path are SBCL-specific — they use sb-posix for termios control and SBCL's ioctl wrapper. cl-tty-kit requires SBCL throughout, so these are available on every build that loads. See Compatibility.

Raw mode

By default a terminal is in cooked (canonical) mode: it buffers a line, echoes keystrokes, and interprets control characters like C-c locally. A full-screen application needs the opposite — every byte delivered immediately, no echo, no signal interpretation — which is raw mode.

enable-raw-mode and disable-raw-mode toggle it for a file descriptor (defaulting to standard input, fd 0). enable-raw-mode snapshots the current termios settings before changing them, and disable-raw-mode restores that snapshot exactly.

with-raw-mode

Prefer the scoped macro, which guarantees restoration with unwind-protect even if the body unwinds:

(with-raw-mode (0)
  ;; standard input is now byte-transparent; read and decode here
  ...)
;; the terminal is restored to its prior settings on the way out

Reference-counted nesting

Nested with-raw-mode scopes, or repeated enable-raw-mode calls on the same fd, are reference-counted. Each enable increments a depth counter; the terminal is only restored when the outermost scope exits. This lets independent components each request raw mode without fighting over who restores it:

(with-raw-mode (0)            ; depth 1 — terminal enters raw mode
  (with-raw-mode (0)          ; depth 2 — no-op, just increments
    ...)
  ...)                        ; inner exit — depth back to 1, still raw
;; outer exit — depth 0, terminal restored

Failure reporting

If a termios operation fails, both functions signal raw-mode-operation-failed carrying the operation (:enable or :disable), the fd, and the underlying reason condition — so a caller can report precisely what failed on which descriptor.

More raw than cfmakeraw

The raw configuration clears a strict superset of the classic cfmakeraw input flags — additionally disabling break handling, input marking, stripping, and CR/NL translation. That leaves the input stream fully byte-transparent, which is exactly what a terminal multiplexer needs when it feeds the bytes verbatim to a child PTY. Every added flag only removes input processing, so it remains a valid raw mode for ordinary callers.

terminal-size

terminal-size queries the current window size with ioctl(TIOCGWINSZ) and returns (values columns rows):

(terminal-size)     ; => 80, 24   (on a real terminal)
(terminal-size 0)   ; same, fd given explicitly

It returns (values nil nil) — rather than signaling — when the fd is not a terminal, the ioctl fails, the report is zero-sized, or the platform's TIOCGWINSZ constant is unknown. Treat nil as "size unavailable" and fall back to a default (commonly 80×24) or a cursor-position probe:

(multiple-value-bind (cols rows) (terminal-size)
  (let ((cols (or cols 80))
        (rows (or rows 24)))
    ...))

To react to live resizes, re-query on SIGWINCH.

with-terminal-session

with-terminal-session is a thin composition helper that wraps a body in the common full-screen lifecycle: enter the alternate screen, hide the cursor, flush output, run the body, and then guarantee teardown with unwind-protect — in reverse order — no matter how the body exits.

The macro binds a stream variable and takes keyword options:

(with-terminal-session (session :stream *standard-output*)
  (write-string (render-screen screen) session))
;; on exit: cursor shown, alternate screen exited, output flushed

Options

Option Default Effect
:stream *standard-output* Where session escapes and body output are written.
:alternate-screen t Enter/leave the alternate screen buffer.
:hide-cursor t Hide the cursor for the session, restore on exit.
:bracketed-paste nil Enable bracketed-paste markers, scoped to the session.
:keyboard-enhancements nil Push kitty keyboard enhancement <flags>; auto-popped on cleanup.
:raw-mode nil Wrap the whole session in with-raw-mode.
:fd 0 The fd used when :raw-mode t.

Setup steps are attempted defensively: if one step fails, it is left un-done so its matching teardown is skipped and the body still runs. Teardown always emits the reverse sequence for whatever setup actually succeeded and flushes the stream.

Bracketed paste and keyboard enhancements

:bracketed-paste t turns on the DEC private-mode paste markers for the duration of the body and disables them on exit, so paste input arrives bracketed only while your session owns the terminal. :keyboard-enhancements <flags> pushes a kitty keyboard progressive-enhancement scope with ansi-push-keyboard-enhancements on entry and pops it with ansi-pop-keyboard-enhancements on exit — a clean push/pop pair so you never leak enhancement state to the shell.

Composing with raw mode

Pass :raw-mode t (and, if not fd 0, :fd n) to wrap the entire session in with-raw-mode. This is the usual full-screen setup — raw input plus alternate screen plus hidden cursor — in one form:

(with-terminal-session (session :raw-mode t :fd 0
                                :bracketed-paste t
                                :keyboard-enhancements 1)
  ;; input is raw here; drive your event loop and render frames
  (write-string (render-screen screen) session))

Example: a scoped session

examples/terminal-session.lisp renders a small screen inside a session with bracketed paste and keyboard enhancements enabled. Note that everything — alternate screen, cursor hide, paste markers, and enhancement flags — is scoped to the body and torn down automatically:

(let ((screen (make-screen 18 3)))
  (screen-write-string screen 0 0 "TTY" :style '(:bold))
  (screen-write-string screen 4 0 "demo")
  (screen-write-string screen 0 1 "Press")
  (screen-write-string screen 6 1 "q to")
  (screen-write-string screen 11 1 "exit")
  (with-terminal-session (session :stream *standard-output*
                                  :bracketed-paste t
                                  :keyboard-enhancements 1)
    (write-string (render-screen screen) session)))

The companion macro with-terminal-session-output captures the emitted escape sequences into a returned string instead of writing to a live terminal, which is handy in tests and examples.

See also

  • PTY — spawning and driving a child process under a PTY.
  • Screen and Rendering — the render-screen / render-diff output you emit inside a session.
  • ANSI Helpers — the individual escape builders with-terminal-session composes.
  • Conditionsraw-mode-operation-failed and the condition hierarchy.
  • Compatibility — the SBCL-only rationale.