Help us shape the future of Go! Take the 2026 Go Developer Survey

io

package
v0.38.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2026 License: Apache-2.0 Imports: 12 Imported by: 1

Documentation

Index

Constants

View Source
const (
	// DefaultGuestBufferSize bounds how much output may sit in pending before a
	// guest is dropped; the worst case for undelivered output is this plus one
	// in-flight maxDrainChunk. A guest merely slower than whatever paces the
	// session — the primary host terminal, or with none the earliest guest —
	// rather than stalled, accrues backlog at the difference between that pace
	// and its own drain rate, and a sustained mismatch exhausts any finite cap:
	// this number sets how long a slow guest survives, not whether. The pacing
	// guest adds no backlog while it paces, since the pty is held to its
	// drain, but it can inherit one of up to the cap when it takes over from
	// a primary or an earlier pacer, and output after pacing is released at
	// exit is unpaced. Total slack is roughly 5 MiB, the cap plus a 2 MiB
	// SSH window on each of the two legs between host and guest. The buffer
	// exists to decouple the fan-out from a blocking write, not to store the
	// session.
	DefaultGuestBufferSize = 1 << 20 // 1 MiB
)
View Source
const DefaultReplayBytes = 256 << 10

DefaultReplayBytes bounds the replay ring handed to a joining writer.

It must stay well under DefaultGuestBufferSize: Append replays into a joining writer before attaching it, so a ring at or above a guest's sink size would overflow that guest with its own replay and drop it at the door.

View Source
const ResetFlushTimeout = time.Second

ResetFlushTimeout bounds how long Shutdown waits for a member's reset to be delivered once its tail has been. Shutdown can therefore return this much past its context's deadline. A reset is a few hundred bytes at most, but the tail may have used up the guest's SSH window, and then the reset waits a round trip for the window to open again: so a whole guest flush's bound, the one the host gives a guest's tail, not a guess at how fast a small write is.

Variables

View Source
var (
	// ErrOverflow is returned once a sink has passed its cap. A guest that
	// cannot receive output is not attached in any useful sense, so the sink
	// fails permanently rather than dropping bytes out of the middle of a
	// terminal stream, which the guest could not detect.
	ErrOverflow = errors.New("asyncwriter: buffer overflow")

	// ErrWriterClosed is returned by Write after Close.
	ErrWriterClosed = errors.New("asyncwriter: closed")

	// ErrStalled is the error a caller enforcing a delivery deadline passes to
	// AbortIfNoProgress. It becomes Err() and reaches onDrop like any other
	// failure.
	ErrStalled = errors.New("asyncwriter: no progress within the stall timeout")
)
View Source
var ErrClosed = errors.New("multiwriter: closed to new writers")

ErrClosed is returned by Append once Shutdown has run. A guest that reaches the door as the session is ending is refused rather than attached to a fan-out nothing will flush again.

Functions

func NewContextReader

func NewContextReader(ctx context.Context, r io.Reader) io.Reader

Types

type AsyncWriter added in v0.27.0

type AsyncWriter struct {
	// contains filtered or unexported fields
}

AsyncWriter delivers to one writer from one goroutine, so writing to it never blocks on that writer's I/O.

It exists because the host fans its pty output out to every guest serially: a guest that stops reading its SSH channel blocks in Write once the channel window fills, and holds up every writer behind it, including the host's own terminal. Handing off into a bounded buffer means the slowest viewer no longer sets the pace. See owenthereal/upterm#524.

The buffer is bounded in bytes rather than in writes, because pty writes run from one byte to io.Copy's 32 KiB and a count of writes is therefore not a bound on memory at all.

func NewAsyncWriter added in v0.27.0

func NewAsyncWriter(w io.Writer, max int, onDrop func(error)) *AsyncWriter

NewAsyncWriter starts delivery to w immediately. max bounds the payload held undelivered; onDrop, which may be nil, is called once if the sink dies, always on its own goroutine so the caller of Write is never held up by it.

The caller owns w. Close stops the goroutine but does not close w.

func (*AsyncWriter) AbortIfNoProgress added in v0.33.0

func (a *AsyncWriter) AbortIfNoProgress(since uint64, err error) bool

AbortIfNoProgress fails the writer, exactly as an overflow does — sticky error, onDrop once, on its own goroutine — but only if Delivered still equals since. It reports whether it aborted. The check and the failure happen under one lock, so a piece delivered after the caller's last snapshot always wins.

A nil err aborts with ErrStalled: a writer counts as failed only once it holds an error, so failing it with nil would discard the queue and spend onDrop while leaving it live.

func (*AsyncWriter) Backlog added in v0.33.0

func (a *AsyncWriter) Backlog() Backlog

Backlog reports how much output this writer is carrying and how far delivery has gotten, for a caller pacing on it rather than merely waiting for it to catch up.

func (*AsyncWriter) Close added in v0.27.0

func (a *AsyncWriter) Close() error

Close stops accepting writes and signals the drain. It deliberately does not wait for the goroutine to exit: in the case this type exists for, that goroutine is blocked in a write that only session teardown will release, and waiting here would deadlock exactly then. The goroutine exits when its in-flight write returns.

Close always returns nil and is safe to call repeatedly. Pending bytes are discarded; MultiWriter.Shutdown is the path that delivers a tail.

func (*AsyncWriter) Closed added in v0.30.0

func (a *AsyncWriter) Closed() bool

Closed reports whether Close has been called.

Like Err, it is the other half of what Flush does not say: Flush returns nil for a closed sink too, and a caller that needs to know whether what it wrote was delivered rather than discarded has to ask.

func (*AsyncWriter) Drained added in v0.30.0

func (a *AsyncWriter) Drained() (<-chan struct{}, bool)

Drained returns a channel closed the next time delivery catches up: the buffer empties and the last write returns. It is Flush for a caller that wants to be told rather than to wait — one holding a lock, or one with nothing to do until the news arrives.

The second return is false for a writer that has already failed or been closed, and there is nothing else it could be: the signal those publish is the one the writer had at the time, and the channel that replaces it is never closed by anything. Answered under the lock the two of them take, so a caller that is told true holds a channel that failing and closing will still close.

func (*AsyncWriter) Err added in v0.30.0

func (a *AsyncWriter) Err() error

Err reports the error that ended delivery, if any.

Flush cannot answer this: it reports nil for a sink that has already failed, because a guest that is gone is not a shutdown error. A caller that needs to know whether everything written was actually delivered — rather than merely that nothing more is coming — has to ask here as well.

func (*AsyncWriter) Flush added in v0.27.0

func (a *AsyncWriter) Flush(ctx context.Context) error

Flush waits until everything written so far has been delivered, or until ctx is done.

It waits on the idle signal the drain publishes once it has emptied the buffer and its last write has returned, rather than polling, and it honours ctx so that one stuck guest cannot hold up the host's exit. A dead or closed sink flushes to nil: there is nothing left to deliver and a guest that is already gone is not a shutdown error.

func (*AsyncWriter) Write added in v0.27.0

func (a *AsyncWriter) Write(p []byte) (int, error)

Write copies p into the pending buffer and returns. It never performs I/O and never blocks on the underlying writer.

It reports overflow as an error so that MultiWriter, which already drops a writer that fails, removes this one without needing to know anything about buffering.

type Backlog added in v0.33.0

type Backlog struct {
	Bytes     int             // pending plus the undelivered rest of the chunk in flight
	Delivered uint64          // total bytes delivered; advances with every piece
	Progress  <-chan struct{} // closed after the next delivered piece, or on fail or close
	Live      bool            // false once failed or closed
}

Backlog reports what a caller pacing on this writer needs to know: how much output is not yet delivered, how far delivery has gotten, and a channel to wait on for the next change.

type Flusher added in v0.27.0

type Flusher interface {
	Flush(ctx context.Context) error
}

Flusher is implemented by attached writers that deliver asynchronously and so can still be holding output when the producer stops.

type ModeTracker added in v0.30.0

type ModeTracker struct {
	// contains filtered or unexported fields
}

ModeTracker watches terminal output and remembers the modes set on it, so a reader joining later can be put into the same modes before it is handed the replay ring.

It is deliberately not a terminal emulator: it reconstructs no screen and no cursor position. Anything it does not recognise passes through unrecorded.

Not safe for concurrent use. MultiWriter owns one and only touches it under writeMu.

func NewModeTracker added in v0.30.0

func NewModeTracker() *ModeTracker

func (*ModeTracker) Restore added in v0.30.0

func (m *ModeTracker) Restore() []byte

Restore returns the bytes that put a terminal this tracker has been watching back where it started. It is Snapshot's opposite: Snapshot is for the terminal joining a session, this is for the terminal leaving one.

A terminal is left in whatever modes the session put it in, and restoring the termios settings says nothing about them: a full-screen program that was on screen when its viewer detached leaves that viewer's shell drawing on the alternate screen, with no cursor, with the mouse reporting clicks as input, and confined to the rows the program had chosen for itself. None of that is the shell's doing and none of it goes away on its own.

Only what was actually changed is undone, so a session that never touched a mode produces nothing at all — the same property Snapshot has, and the reason this is derived from the stream rather than being a fixed list of resets sent on the way out. Attributes are not tracked and so are not reset here.

"Where it started" means the terminal's power-on defaults, not the state the caller's own terminal was in before the attachment. That state is not knowable without asking the terminal for it — a DECRQM round-trip per mode, before attaching, racing the user's keystrokes on the same stdin, and unanswered by terminals that do not implement it. tmux, which has the same problem on detach, resets to defaults too rather than asking.

It costs less than it sounds like. At a shell prompt — which is what launches a CLI — every mode in restorable is already at its default except the ones the shell re-asserts on every prompt: measured here, zsh holds DECCKM and bracketed paste and emits both again at the next prompt, bash holds bracketed paste and does the same. So for the modes a caller actually holds, a reset is undone within one keystroke, and for the rest the default is what the caller had. What is left is a caller that is not a shell and keeps the mouse or the alternate screen while running a child, which is not how a program that shells out behaves.

Being inside a sequence is state too. Output can stop anywhere — between the chunks a suspend falls between, or at a session's last byte — and a terminal left inside a sequence reads whatever the shell prints next as the rest of it: an unfinished OSC swallows the prompt whole. CAN cancels a sequence from any state on the DEC parser terminals implement, and does nothing outside one.

The keyboard is state too. A program that asked for the kitty keyboard protocol or modifyOtherKeys and never put them back leaves the shell it returns to receiving Ctrl-letter keys, and with kitty's flags most others, as escape sequences it does not read. The kitty stacks are popped whole, in one pop each: a pop past the bottom empties a stack, which is where a terminal starts, so the depth this tracker recorded is safe to pop even on a terminal that was replayed fewer entries than that.

The order is the order a terminal has to receive it in: out of any sequence first, so the rest is not read as part of it; then the alternate screen's keyboard stack, popped while that screen is still the one showing, since a pop applies to the current screen's stack; then off the alternate screen, through the mode that entered it, since everything after that applies to the screen the terminal is going back to, the main screen's stack included.

func (*ModeTracker) Snapshot added in v0.30.0

func (m *ModeTracker) Snapshot() []byte

Snapshot returns the bytes that put a fresh terminal into the recorded modes. State already at the terminal's default is left out, so a session that never changed anything replays nothing. Its length is bounded by len(restorable) plus the screen switch, the three verbatim sequences, two kitty keyboard stacks of kittyStackDepth entries, modifyOtherKeys and one partial sequence. Only the partial is sized by the stream rather than by this file, and its own bound is maxSequenceBytes for a mode sequence or maxStringBytes for a string: the ceiling is a few kilobytes, not the hundreds of bytes everything else comes to.

The order is the order the stream would have had to use to reach this state: the normal screen's margins, then the modes, then the normal screen's kitty keyboard stack and modifyOtherKeys, then the switch to the alternate screen, then that screen's own margins and keyboard stack, and only while it is the one showing. While the normal screen is showing, the alternate screen's stack is replayed on a trip there and back, because a push applies only to the screen that is showing.

The state as of the ring's first byte includes being partway through a sequence, so the partial goes last, after the charset. The ring's first bytes then complete it on the joiner's terminal instead of printing as text -- which is what they did when the trim boundary fell inside a sequence and the snapshot said nothing about it.

func (*ModeTracker) Write added in v0.30.0

func (m *ModeTracker) Write(p []byte) (int, error)

Write consumes output and records mode changes. It never fails and never alters the stream; callers use it as an observer, not a filter.

type MultiWriter

type MultiWriter struct {
	// contains filtered or unexported fields
}

MultiWriter is a concurrent safe writer that allows appending/removing writers. Newly appended writers get the last write to preserve last output.

Attached writers are tracked by identity, so they must be comparable; see checkRemovable, which is how Append enforces it. NewMultiWriter does not, because it cannot report the error, so prefer Append.

func NewMultiWriter

func NewMultiWriter(replayBytes int, writers ...io.Writer) *MultiWriter

NewMultiWriter returns a fan-out whose replay ring holds the most recent replayBytes bytes of output.

func (*MultiWriter) Append

func (t *MultiWriter) Append(writers ...io.Writer) error

Append attaches writers, handing each the replay buffer first so it starts from recent output rather than mid-screen.

Both steps happen under writeMu, the fan-out lock, so attaching is atomic with respect to a Write: a joining writer gets the replay and every write after it, never a write that is also in its replay and never a gap between them. Holding the fan-out lock here is only safe because attached guest writers no longer block on I/O; under the old design it would have reintroduced the deadlock #523 removed.

func (*MultiWriter) Remove

func (t *MultiWriter) Remove(writers ...io.Writer)

func (*MultiWriter) Shutdown added in v0.27.0

func (t *MultiWriter) Shutdown(ctx context.Context) error

Shutdown closes the fan-out to new writers and then waits for everything already accepted to be delivered.

The two halves are inseparable. Flushing a snapshot alone would leave a window: the SSH server keeps serving while the flush runs, so a guest attaching after the snapshot has its replay queued into a sink this call will never flush, and teardown closes it before delivery. Quiescing under writeMu, the same lock Append takes, leaves no such window — an attach is either inside the snapshot or refused.

Members that do not buffer are skipped: a synchronous writer is delivered by definition. A sink that has already failed flushes to nil, because a guest that is already gone is not a shutdown error.

Then each member that wants one (ResetTarget) is sent what puts its terminal back where it started -- the modes the session left set, undone -- and flushed again. A session can end with its command still holding the terminal, killed on the alternate screen with the mouse on, and a guest's terminal is not one anything else will put back. A member's reset waits for its own first flush: queued behind a tail, it could take a sink that was going to drain past its bound, and a sink that overflows is a guest dropped. It waits for nothing else, so a guest stuck until the deadline costs the others nothing; and one still behind at the deadline loses the reset with its tail, as it would have lost the tail anyway. The reset's own flush gets ResetFlushTimeout, which may run past ctx's deadline.

func (*MultiWriter) Write

func (t *MultiWriter) Write(p []byte) (int, error)

Write fans p out to every attached writer. It always reports success.

Two things here are deliberate, and both were bugs.

The membership lock is released before any writer is written to. Holding one lock for both jobs made Append and Remove wait on whatever the slowest attached writer was doing, and a guest that stops reading its SSH channel blocks in Write indefinitely once the channel window fills. That wedged the host: HandleSession removes its writer on the way out, so Remove blocked, HandleSession never returned, and the client-left event it emits on the way out was never sent. Writes are still serialized among themselves, by writeMu, because concurrent producers must not interleave in a writer or race one that is not concurrency safe.

A failing writer is dropped rather than reported. This is a broadcast to whoever is attached, and the producer is the host's pty: returning an error aborted the io.Copy feeding it, which ended the command and tore down the whole session. One guest losing its connection at the wrong moment must not take the session with it. Errors used to abandon the rest of the slice too, so a broken guest silenced everyone attached after it.

A writer removed between the snapshot and the write still receives this one write. That is harmless: it is a session on its way out.

Writers that buffer are what keep this serial loop honest: each attached guest is an AsyncWriter, so its Write is a copy and a signal rather than SSH I/O, and a guest that cannot keep up overflows and is dropped instead of pacing everyone else. The host's own stdout is attached unwrapped only when it is a terminal: the pty should not run ahead of the screen that owns it. A non-terminal stdout is wrapped, because a pipe nobody drains would otherwise block here with writeMu held and wedge the whole session. With no primary, a pacer in front of Write holds the pty to the earliest guest's drain instead; Write itself still never blocks on a guest.

type ResetTarget added in v0.37.0

type ResetTarget interface {
	WantsReset() bool
}

ResetTarget is implemented by an attached writer that wants the session's reset when the fan-out shuts down: one whose far end is a terminal nothing else will put back. A guest's is the one -- behind a plain ssh client, which restores termios on the way out and nothing more. Every other member is sent none: a viewer capturing the output to a file gets what the command wrote, and a local client puts its own terminal back. Its Write must not block, as an AsyncWriter's does not: Shutdown writes the reset with writeMu held.

type StallWriter added in v0.30.0

type StallWriter struct {
	// contains filtered or unexported fields
}

StallWriter writes synchronously to w in chunks, stamping progress after each one, and calls onStall once if a write has been outstanding with no chunk completed for timeout.

It exists for the primary client's pacing. The primary's writer is attached to the fan-out synchronously, so the command cannot outrun the terminal, and MultiWriter.Write holds writeMu across it: a primary that stops reading its SSH channel therefore blocks every other writer and every new attach. The bound has to measure progress rather than duration — "a write took 5 s" and "no chunk has completed for 5 s" treat a slow-but-working terminal differently, and only the second is worth acting on — and it has to be armed only while a write is outstanding, or a healthy shell at a prompt would be judged stalled for producing nothing.

A single blocking Write is unobservable from outside, which is why the writer chunks: the chunk is the unit of progress. The progress clock starts with each write, so the first chunk after a quiet period is not judged against a stale stamp.

onStall runs on the watchdog's goroutine and must not take writeMu: the lock is held by the write it is there to release. Closing the client's SSH connection is what it is for; see host/internal.

func NewStallWriter added in v0.30.0

func NewStallWriter(w io.Writer, chunk int, timeout time.Duration, onStall func()) *StallWriter

NewStallWriter starts the watchdog. chunk bounds the bytes handed to w per call; onStall may be nil.

func (*StallWriter) Close added in v0.30.0

func (s *StallWriter) Close() error

Close stops the watchdog. It does not close w and does not release a write already blocked in it; only onStall's action can do that.

func (*StallWriter) Write added in v0.30.0

func (s *StallWriter) Write(p []byte) (int, error)

Write hands p to w in chunks and reports a short write from w as an error, the way MultiWriter would refuse it.

type TerminalQueryFilter added in v0.21.0

type TerminalQueryFilter struct {
	// contains filtered or unexported fields
}

TerminalQueryFilter wraps an io.Writer and filters out terminal query sequences from the output. This prevents queries sent by the host's shell from reaching connected clients, whose terminals would otherwise respond and pollute the PTY input.

What counts as a query is decided by family, in query_classify.go. The filter itself only follows the stream the way a terminal's DEC parser does -- where each sequence starts, where it ends, and what the terminal does with the bytes in between -- and holds a sequence until its classifier can decide it: an ESC sequence or a CSI to its final byte, an OSC to its terminator, a DCS to its header's final byte, and a kitty graphics command (an APC whose first byte is G) to the end of its control data, or to a BEL before it that settles it as a query. A DCS or APC that is a query is then dropped as it streams, to the string's end (drop mode), and one that isn't is passed as it streams, as a PM or SOS is. So a sixel or an image is never buffered, nor is a query's payload.

A tmux passthrough wrapper, ESC P tmux; then a sequence with each ESC doubled, then ESC \, is held while an inner instance of this machine, fed the wrapped stream decoded, decides the first sequence in it. If that is no query, the wrapper passes byte for byte. If it is, the wrapper's head and the query are dropped, and the rest of the wrapper goes out as ordinary output, filtered from ground: a DEC terminal ends the wrapper's DCS at its first ESC and reads the rest live, so that removes exactly the query.

Terminals disagree on where a malformed string ends. A DEC parser ends one at a CAN, a SUB or an ESC that isn't ST; kitty keeps any string open there, and tmux a DCS, and each takes what follows as more of it. So an OSC, a DCS header or an APC the filter is still holding when it ends one of those ways is dropped whole, query or not: passed through, "ESC ]4;0; CAN ;1;? BEL" is a palette query to kitty. What that costs is a malformed OSC's effect on a DEC terminal, such as a title ended by a stray ESC.

A sequence the filter has emitted before its end -- a string it passes, a tmux wrapper among them, a CSI a terminal ignores, or one it gave up on past its bound -- is always closed. The terminal stays inside it until it sees the ESC that ended it in the input, and that ESC starts a sequence the filter holds. If the held sequence passes, its ESC is the close; if it is dropped, the filter sends ESC CAN in its place, whose ESC ends the emitted sequence the way the input's did and whose CAN cancels the escape it starts. Without the close, whatever came after a dropped query landed inside the emitted sequence.

func NewTerminalQueryFilter added in v0.21.0

func NewTerminalQueryFilter(w io.Writer) *TerminalQueryFilter

NewTerminalQueryFilter creates a filter that removes terminal query sequences from output before writing to the underlying writer.

func (*TerminalQueryFilter) Dropping added in v0.38.0

func (f *TerminalQueryFilter) Dropping() bool

Dropping reports whether the filter is discarding a query string, at top level or inside a tmux wrapper, that hasn't ended yet. Pending then holds the head that decided the drop, which must not reach a terminal that would answer it, so a sink promoted to primary now settles the drop with SettleDrop rather than handing Pending over.

func (*TerminalQueryFilter) Pending added in v0.30.0

func (f *TerminalQueryFilter) Pending() []byte

Pending returns the bytes that put a fresh filter in this one's state. Fed everything this filter has written, then Pending(), a fresh filter ends in the state this one is in: where it is in a sequence, the match so far against "tmux;" at the start of a DCS, where it is in a tmux wrapper and in the sequence the wrapper carries, whether it is dropping a string or passing a sequence it gave up on, and whether it owes a close. A joiner's filter takes its state from the result (MultiWriter.Append), and a newly promoted primary is handed it raw (hostSink.promote), unless the filter is Dropping, where SettleDrop takes its place. What it returns depends on the state:

  • While deciding, the held bytes: a head, an OSC to its terminator, a DCS header to its final byte, an APC's ESC _ before its first byte, or a graphics command with its control data so far, BELs that asked nothing among them.
  • In drop mode, the head that decided the drop, and the ESC held to see whether a '\' follows, if there is one. The head is a DCS header, or a graphics command's control data with the ';' or BEL that decided it. A joiner's filter fed the head drops the rest of the string as this one does, and one that joins between a terminator's ESC and its '\' still ends the string there.
  • While a tmux wrapper is being decided, its raw bytes as held: the framing, the bytes before its first inner ESC, and the inner sequence's head with its ESCs still doubled, a raw ESC whose pair is still to come included. A joiner's filter re-decodes them to the same state, the control bytes the inner head forwarded included, which have not gone out yet. While a DCS whose header ended in 't' is matching "tmux;", the header and the match so far.
  • While a wrapper's first inner sequence is dropped, the wrapper's framing, the inner head that decided the drop with its ESC doubled, and then, in that order, the inner parser's pending ESC as ESC ESC, if it holds one, and a raw ESC whose pair is still to come, if there is one. The pending ESC goes in though its raw pair is complete and leaves no raw ESC unpaired, so a joiner still ends the inner string where this filter does: without it, the joiner would drop what follows as payload. The control bytes the inner head forwarded have gone out, and aren't in it.
  • In a pass state, where the head went out with the stream, an ESC held to see what the next byte makes of it, or nothing. A DCS, a DCS-ignore, an APC past its ';' or one that isn't a graphics command, a PM or SOS, a tmux wrapper passing, a CSI the terminal ignores and a sequence past its bound are all in this state.
  • With a close owed, the held sequence that will settle it. It begins with the ESC that ended the emitted sequence, so the joiner's filter owes the same close.

The result restores a parse state; it does not recover history. It never contains a byte the filter discarded, a dropped string's payload among them, since those are gone and are not coming back, nor a C0 it forwarded, which went out ahead of the hold and would be sent twice. A dropped string's head is in it only as state, which a joiner's filter needs to drop the rest. It is a copy, and never longer than maxPendingBytes.

func (*TerminalQueryFilter) SettleDrop added in v0.38.0

func (f *TerminalQueryFilter) SettleDrop(p []byte) (n int, raw []byte, done bool)

SettleDrop is the handoff for a sink promoted to primary while Dropping. The bytes already dropped can't be recovered, so the rest of that string is no use to the new primary; everything after it is. SettleDrop consumes from p only the rest of the string being dropped, and parses nothing after the string's end: the byte after a restarting ESC is not looked at as part of a new sequence. Nothing goes to the filter's writer.

  • done reports whether the string ended within p. If not, n == len(p) and raw is nil: all of p was part of the drop, and the filter is still Dropping, for the next call.
  • When it did, the caller writes raw, then p[n:], unfiltered, and never uses the filter again.
  • The general rule: the stream resumes at a raw resume point. If that point is in p, it is p[n] and raw is nil. If it lies in bytes the filter carried from an earlier write, raw holds the carried raw bytes from that point on, and n is 0. Those are one or two raw ESCs, in a new slice the caller owns.

Where it ended fixes the resume point:

  • at ST: just after the '\', so p[:n] ends with it;
  • at CAN or SUB: at that byte;
  • at an ESC that isn't ST: at that ESC. It's the carried byte when the filter held it from the last write, so raw is "\x1b" and p[0] is the byte after it.
  • inside a wrapper: after an inner ST or the outer ST, at a CAN or SUB, and at a lone raw ESC x, which ends the wrapper. An inner ESC that isn't ST resumes at the second raw ESC of its pair, which is carried when the pair completed in the last write. For example, promoted after "\x1bPtmux;\x1b\x1bP+q544e\x1b\x1b" and given "[6nhello\x1b\\", SettleDrop returns raw "\x1b", n 0, done true. An inner ESC that ends the string by following a pending one resumes at the pending one's second raw ESC, so raw can be "\x1b\x1b".

A filter that isn't Dropping, a fresh one or one SettleDrop has already returned done for among them, has no string to settle: SettleDrop returns 0, nil and true, and the stream resumes at p[0]. It hands over nothing the filter holds, though: Pending, if there is any, is still the caller's to give the new primary raw, as hostSink.promote does when the filter isn't Dropping, since it may be the lead-in of a query the primary should answer.

func (*TerminalQueryFilter) Write added in v0.21.0

func (f *TerminalQueryFilter) Write(p []byte) (int, error)

Write filters terminal query sequences from p and writes the result to the underlying writer. Returns len(p) on success to indicate all input bytes were processed. On error, returns 0 because the filtered output is written atomically (all or nothing) and input bytes don't map 1:1 to output bytes due to filtering.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL