KetraTerm Session (:ketraterm-session)
TerminalSession connects a transport connector to parsing, terminal state,
input encoding, and render publication. It serializes terminal mutation and
orders application input, terminal replies, and startup input through one
bounded writer. It has no dependency on Swing or a PTY implementation.
For local processes, use the PTY module. For an
existing transport, supply a TerminalConnector
as shown below. See the root README for dependency selection.
Usage
This example runs an already-created connector until closure. copyFrame copies
the borrowed frame into storage owned by the renderer; it must return promptly
and retain neither the cache nor its arrays.
import io.github.ketraterm.core.TerminalBuffers
import io.github.ketraterm.render.cache.TerminalRenderCache
import io.github.ketraterm.session.TerminalSession
import io.github.ketraterm.session.TerminalSessionCloseEvent
import io.github.ketraterm.session.TerminalSessionState
import io.github.ketraterm.transport.TerminalConnector
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
suspend fun runTerminal(
connector: TerminalConnector,
copyFrame: (TerminalRenderCache) -> Unit,
): TerminalSessionCloseEvent = coroutineScope {
val terminal = TerminalBuffers.create(width = 80, height = 24)
val session = TerminalSession.create(terminal = terminal, connector = connector)
val frames = launch {
session.renderGeneration.collect {
session.readPublishedFrame(copyFrame)
}
}
try {
session.start(columns = 80, rows = 24)
session.requestRender(scrollbackOffset = 0)
session.state.filterIsInstance<TerminalSessionState.Closed>().first().event
} finally {
frames.cancel()
session.close()
}
}
The connector owns inbound delivery. Do not also feed bytes into the session
from another producer. start is synchronous and permits one attempt; it may
block in the connector. Input is accepted only after connector startup succeeds.
Startup failure closes the session and rethrows the failure.
Successful construction transfers exclusive runtime access to the mutable
terminal and connector. Configure the core before assembly; afterward, use
session APIs for input, settings, and frame reads. A failed create leaves the
connector with its caller. Session closure cancels session workers but does not
close the supplied dispatchers or cancel consumer collection scopes.
Input and lifecycle
Use submitInput with semantic key, mouse, focus, paste, or text-replacement
events. A list of events is admitted as one operation whose bytes cannot
interleave with later input or replies. submitBytes is for trusted, already
encoded host input: it copies the requested slice without paste framing or
sanitization. Existing encode* methods use the same admission path and discard
its result.
TerminalInputAdmission.ACCEPTED means queue admission. It does not confirm a
connector write or shell execution, and includes empty or mode-suppressed input.
Other results distinguish incomplete startup, closure, and capacity exhaustion.
Capacity exhaustion closes the session without admitting the rejected operation.
Transport failure or closure can interrupt accepted output; bytes are not retried.
See admission and ordering
for budgets and conditional editing.
Observe state for Created, Running, and retained Closed(event).
isClosed becomes true when shutdown starts; Closed is published after cleanup
and final-frame publication have been attempted. State flows do not complete on
closure, so consumers own cancellation of their collectors. Calls to close
are idempotent, but a concurrent close can return before the first close finishes.
UI disposal can call requestClose(): it claims termination immediately and runs
cleanup on cleanupDispatcher, independently of blocked connector writes.
Its default is a shared elastic I/O view with a separate parallelism limit.
Observe Closed for completion and cleanup failures. Custom dispatchers must run
blocking work off the UI thread independently of connector writes. Tests using
controlled dispatchers should inject this dispatcher as well as the worker and
I/O dispatchers.
Retained output remains readable after closure. See closed-session presentation.
Rendering and local operations
Terminal settings, host policy, geometry and local clearBuffer() calls are
synchronous and can wait for image decoding. Run them off UI threads through an
ordered owner, then report completion to the UI. Use withMutationAdmission to
recheck a captured binding/request token after the wait. Its callbacks must not
access UI state or wait for UI work. Frame reads, ordinary input, and input/paste
policy updates remain available during decode. Connectors must deliver output
off UI threads, including output delivered synchronously during start.
readRenderFrame borrows live terminal data under mutation serialization.
readPublishedFrame borrows the latest copied cache and returns null before the
first publication. Both are safe from any thread, including after closure.
Read or copy only within the callback; do not retain borrowed storage, mutate it,
wait for UI work, close the session, or reenter session mutation.
renderGeneration identifies successful publications. requestRender replaces
the session's requested viewport; one session has one published viewport.
Independent scroll positions cannot share that publication. A synchronous frame
read with an explicit offset does not change the published viewport.
See render scheduling.
resizeViewport returns the new scrollback offset, history size, and discarded
row baseline together; apply the complete result when preserving scroll position.
Resize may block in the connector and is not a barrier for queued input.
Session construction, start, resize, and expected-grid pixel updates require at
least TerminalBuffers.MIN_WIDTH (2) columns and one row. Invalid geometry is
rejected before session mutation or connector operations.
The five-argument tryResizeViewport overload accepts text-area pixel width and
height along with the grid dimensions and viewport offset. Supply the measured
cell area excluding padding; either zero means pixels are unavailable. The
existing grid-only resize methods preserve pixels for an unchanged grid and
report unknown pixels when the grid changes. Swing supplies its
measured cell area automatically. Subscribed mode 2048 reports are admitted
after successful core and connector resize, through the ordered writer.
Failed resizes can be retried with the same geometry; completion reports the
current size once if the subscription and policy still permit it.
tryUpdateViewportPixels performs a pixel-only update only when its expected
grid still matches core and no resize is unfinished. A false result leaves
state untouched; use tryResizeViewport and adopt its complete anchor instead.
setThemePalette similarly admits subscribed mode 2031 reports after host theme
updates. See the change-report contract.
clearBuffer clears the active screen and history without sending connector input
or resetting the parser. It returns false after shutdown begins. See the
clear contract before
retaining selection or shell anchors across a clear.
Host-owned shell integration
A plain session has no shell metadata producer. An embedding host can supply its bounded terminal-facing timeline, versioned editing state, and prompt readiness:
import io.github.ketraterm.core.TerminalBuffers
import io.github.ketraterm.session.TerminalSession
import io.github.ketraterm.session.TerminalShellCommandLineState
import io.github.ketraterm.session.TerminalShellIntegrationFactory
import io.github.ketraterm.session.TerminalShellIntegrationState
import io.github.ketraterm.transport.TerminalConnector
import kotlinx.coroutines.flow.StateFlow
fun createHostSession(
connector: TerminalConnector,
shellState: TerminalShellIntegrationState,
commandLine: TerminalShellCommandLineState,
promptReady: StateFlow<Boolean>,
): TerminalSession = TerminalSession.create(
terminal = TerminalBuffers.create(width = 80, height = 24),
connector = connector,
shellIntegration = TerminalShellIntegrationFactory.host(
state = shellState,
commandLine = commandLine,
promptReady = promptReady,
),
)
Use a distinct projection per session and publish metadata in output order.
Publish unknown editing state as null. For asynchronous suggestions, capture
captureCommandEdit() and submit with submitInput(expected, events) so stale
edits are rejected. See conditional edits
and the producer contract.
KetraTerm's optional producer
Add ketraterm-shell-integration and
select OscShellIntegration through shellIntegration when launching a shell
with compatible hooks. Factory selection does not install shell scripts.
Custom assembly
TerminalSession.create(terminal, renderReader, connector) supports separate core
and render collaborators describing the same state. Initial frame dimensions
are checked before ownership transfers; matching dimensions alone do not prove
that the collaborators share state.
inputEncoderFactory creates independent admission and streaming encoders with
session-owned mode sources, output sinks, and input policy. parserFactory
receives the assembled command sink and live clipboard-write budget. Pass both
to TerminalParsers.create when adding a custom OSC handler so built-in services
remain wired. Factories must create fresh instances without I/O or background jobs.
See ordered custom OSC handling.
The low-level constructor accepts caller-assembled parser, response reader,
render publisher, and host adapter. The caller owns their consistent wiring;
the session does not wrap an already-built parser. Use standard create assembly
unless that wiring must be supplied independently.
OSC 52 clipboard reads use an optional TerminalClipboardReader. The host owns
consent and native access; session owns permission checks, deadlines, and bounded
reply commitment. A missing provider produces an empty reply when replies are
permitted. See clipboard lifetime.
For maintainer ownership and validation entry points, see Module.md.