Class: Tempest::REPL::Screen

Inherits:
Object
  • Object
show all
Defined in:
lib/tempest/repl/screen.rb

Overview

Implements the earthquake-style split layout: the bottom row holds the tempest> prompt, while the rest of the terminal scrolls timeline lines in from below. Built on the DECSTBM (top/bottom margin) escape sequence so we don’t need a full curses screen.

Sequences used:

CSI top;bottom r  set scrolling region
CSI r             reset scrolling region (full screen)
CSI row;col H     move cursor
ESC 7 / ESC 8     save/restore cursor (DECSC/DECRC)

Constant Summary collapse

PROMPT_ROWS =

Number of bottom rows reserved for the prompt. Reline may wrap a long input across multiple display rows; without reserving those rows below the DECSTBM region the wrap would emit n past the bottom margin and scroll the timeline off-screen. Two rows is the common case for a single-wrap post.

2

Instance Method Summary collapse

Constructor Details

#initialize(io:, rows: nil, cols: nil) ⇒ Screen

Returns a new instance of Screen.



24
25
26
27
28
29
30
31
32
# File 'lib/tempest/repl/screen.rb', line 24

def initialize(io:, rows: nil, cols: nil)
  @io = io
  @rows = rows
  @cols = cols
  @enabled = false
  @suspended = false
  @mutex = Mutex.new
  @pending_resize = nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, **kwargs, &block) ⇒ Object



153
154
155
# File 'lib/tempest/repl/screen.rb', line 153

def method_missing(name, *args, **kwargs, &block)
  @io.send(name, *args, **kwargs, &block)
end

Instance Method Details

#disableObject



48
49
50
51
52
53
54
55
# File 'lib/tempest/repl/screen.rb', line 48

def disable
  return unless @enabled
  uninstall_resize_trap
  @io.print "\e_Ga=d,q=2\e\\"
  @io.print "\e[r"
  @io.flush if @io.respond_to?(:flush)
  @enabled = false
end

#enableObject



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tempest/repl/screen.rb', line 34

def enable
  return unless @io.respond_to?(:tty?) && @io.tty?
  rows = @rows || detect_rows
  return unless rows && rows >= PROMPT_ROWS + 3

  @rows = rows
  @cols ||= detect_cols
  @io.print "\e[1;#{rows - PROMPT_ROWS}r" # scrolling region: rows 1..rows-PROMPT_ROWS
  @io.print "\e[#{prompt_row};1H"          # park cursor on the first prompt row
  @io.flush if @io.respond_to?(:flush)
  @enabled = true
  install_resize_trap
end

#enabled?Boolean

Returns:

  • (Boolean)


79
80
81
# File 'lib/tempest/repl/screen.rb', line 79

def enabled?
  @enabled
end

#flushObject



141
142
143
# File 'lib/tempest/repl/screen.rb', line 141

def flush
  @io.flush if @io.respond_to?(:flush)
end

#notify_resize(rows: nil, cols: nil) ⇒ Object

SIGWINCH hook. Trap handlers in Ruby are restricted (can’t reliably acquire mutexes or drive Reline), so we only stash the new dimensions here and apply them on the next mutex-protected write. If rows/cols are omitted (the production path), they’re read from IO.console at apply time so coalesced WINCHes still pick up the latest size.



104
105
106
# File 'lib/tempest/repl/screen.rb', line 104

def notify_resize(rows: nil, cols: nil)
  @pending_resize = { rows: rows, cols: cols }
end

#prepare_promptObject

Clear the rows reserved for the prompt and re-park the cursor on the first prompt row. Called by the REPL right before each readline so a previous wrapped input doesn’t leave residue on the lower prompt rows.



86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/tempest/repl/screen.rb', line 86

def prepare_prompt
  return unless @enabled
  @mutex.synchronize do
    apply_pending_resize
    PROMPT_ROWS.times do |i|
      @io.print "\e[#{prompt_row + i};1H"
      @io.print "\r\e[2K"
    end
    @io.print "\e[#{prompt_row};1H"
    @io.flush if @io.respond_to?(:flush)
  end
end


133
134
135
# File 'lib/tempest/repl/screen.rb', line 133

def print(*args)
  @io.print(*args)
end

#puts(*lines) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/tempest/repl/screen.rb', line 108

def puts(*lines)
  # While `:compose` hands the terminal off to $EDITOR the Jetstream
  # thread keeps emitting events. Swallow them rather than print over
  # the editor's screen; the cursor is persisted, so reconnect-after-
  # exit would replay anyway if anything important was missed.
  return if @suspended
  @mutex.synchronize do
    apply_pending_resize
    if @enabled
      flat = lines.empty? ? [""] : lines.flat_map { |l| l.to_s.split("\n") }
      flat.each { |line| insert_above_prompt(line) }
    else
      # Best-effort write that doesn't shred the prompt when we don't have
      # a scrolling region in place. Reline rerender is invoked by
      # AsyncOutput; Screen itself stays neutral here.
      (lines.empty? ? [""] : lines).each do |line|
        @io.print "\r\e[2K"
        @io.puts line
      end
      @io.flush if @io.respond_to?(:flush)
    end
  end
  rerender_prompt
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


149
150
151
# File 'lib/tempest/repl/screen.rb', line 149

def respond_to_missing?(name, include_private = false)
  @io.respond_to?(name, include_private)
end

#resumeObject



73
74
75
76
77
# File 'lib/tempest/repl/screen.rb', line 73

def resume
  return if @enabled
  @suspended = false
  enable
end

#suspendObject

Transient teardown for handing the terminal off to a subprocess (e.g. $EDITOR via ‘:compose`). Unlike `disable`, this does NOT issue the Kitty graphics delete sequence — terminals that support the Kitty protocol keep image placements in the main screen buffer even while the editor draws on the alternate buffer, so suspending without deleting lets the avatars re-appear automatically when the editor exits. Pair with `resume` to re-establish the scrolling region.



64
65
66
67
68
69
70
71
# File 'lib/tempest/repl/screen.rb', line 64

def suspend
  return unless @enabled
  uninstall_resize_trap
  @io.print "\e[r"
  @io.flush if @io.respond_to?(:flush)
  @enabled = false
  @suspended = true
end

#tty?Boolean

Returns:

  • (Boolean)


145
146
147
# File 'lib/tempest/repl/screen.rb', line 145

def tty?
  @io.respond_to?(:tty?) ? @io.tty? : false
end

#write(*args) ⇒ Object



137
138
139
# File 'lib/tempest/repl/screen.rb', line 137

def write(*args)
  @io.write(*args)
end