Class: Everywhere::LinePump

Inherits:
Object
  • Object
show all
Defined in:
lib/everywhere/line_pump.rb

Overview

Turns the raw byte stream off a pty master into whole lines.

every dev relays the dev server and the desktop shell itself so it can tag each line with its source. That relay has to be line-oriented — a half-line written to the terminal would leave the cursor mid-row and the dock repaints from column 1 — but a pty hands over arbitrary chunks, so this buffers.

Two pty-specific quirks it exists to absorb:

* ONLCR. The slave's line discipline turns every LF into CRLF, so lines
arrive as "text\r\n". The trailing CR is an artifact, not content.
* In-place rewrites. cargo (and anything with a progress bar) redraws a
row by writing CR and the new text without a newline. What the terminal
would have shown is whatever followed the LAST CR, so that is what gets
emitted — one settled line instead of thirty flickering ones.

Pure: feed it strings, get lines back. No IO, no threads, no clock beyond the idle deadline the caller passes in.

Constant Summary collapse

IDLE_FLUSH =

How long a partial line may sit unterminated before it is emitted anyway. Without this a progress bar that never writes a newline — or a prompt like "Overwrite? [Yn]" — would be invisible for as long as it mattered.

0.2

Instance Method Summary collapse

Constructor Details

#initialize(idle_flush: IDLE_FLUSH, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }) ⇒ LinePump

Returns a new instance of LinePump.



28
29
30
31
32
33
34
35
36
37
# File 'lib/everywhere/line_pump.rb', line 28

def initialize(idle_flush: IDLE_FLUSH, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
  # Bytes in, text out. A pty hands over BINARY, and a chunk boundary can
  # land in the middle of a multi-byte character, so buffering and splitting
  # happen on bytes — UTF-8 is self-synchronizing, so a newline is never
  # inside a character — and each settled line is transcoded on the way out.
  @buffer = String.new(encoding: Encoding::BINARY)
  @idle_flush = idle_flush
  @clock = clock
  @touched = nil
end

Instance Method Details

#<<(chunk) ⇒ Object

Feed a chunk; returns the complete lines it produced (without newlines).



40
41
42
43
44
45
46
47
48
49
50
# File 'lib/everywhere/line_pump.rb', line 40

def <<(chunk)
  @buffer << chunk.b
  @touched = @clock.call
  lines = []
  while (index = @buffer.index("\n"))
    raw = @buffer.slice!(0, index + 1)
    line = settle(raw.chomp("\n"))
    lines << line if line
  end
  lines
end

#drainObject

Emit whatever is left, terminated or not. For end-of-stream.



64
65
66
67
68
69
70
# File 'lib/everywhere/line_pump.rb', line 64

def drain
  return [] if @buffer.empty?

  line = settle(@buffer)
  @buffer = +""
  line ? [line] : []
end

#flush_idleObject

Emit a still-unterminated partial line once it has gone quiet. Call this from the relay loop whenever a read times out.



54
55
56
57
58
59
60
61
# File 'lib/everywhere/line_pump.rb', line 54

def flush_idle
  return [] if @buffer.empty?
  return [] if @touched && (@clock.call - @touched) < @idle_flush

  lines = drain
  @touched = @clock.call
  lines
end

#pending?Boolean

Returns:

  • (Boolean)


72
# File 'lib/everywhere/line_pump.rb', line 72

def pending? = !@buffer.empty?