Class: RubyJard::ReplProxy

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_jard/repl_proxy.rb

Overview

A wrapper to wrap around Pry instance.

Pry depends heavily on GNU Readline, or any Readline-like input libraries. Those libraries serve limited use cases, and specific interface to support those. Unfortunately, to serve Jard’s keyboard functionalities, those libraries must support individual keyboard events, programmatically input control, etc. Ruby’s GNU Readline binding obviously doesn’t support those fancy features. Other pure-ruby implementation such as coolline, tty-reader is not a perfit fit, while satisfying performance and boringly stablility of GNU Readline. Indeed, while testing those libraries, I meet some weird quirks, lagging, cursor jumping around. Putting efforts in a series of monkey patches help a little bit, but it harms in long-term. Re-implementing is just like jumping into another rabbit hole.

That’s why I come up with another approach:

  • Create a proxy wrapping around pry instance, so that it reads characters one by one, in

raw mode

  • Keyboard combinations are captured and handled before piping the rest to the pry instance

  • The proxy interacts with Pry’s REPL loop via Pry hooks (Thank God) to seamlessly switch

between raw mode and cooked mode while Pry interacts with TTY.

  • Control flow instructions are threw out, and captured by ReplProcessor.

As a result, Jard may support key-binding customization without breaking pry functionalities.

Defined Under Namespace

Classes: FlowInterrupt, ReplState

Constant Summary collapse

PRY_EXCLUDED_COMMANDS =

Some commands overlaps with Jard, Ruby, and even cause confusion for users. It’s better ignore or re-implement those commands.

[
  'pry-backtrace', # Redundant method for normal user
  'watch',         # Conflict with byebug and jard watch
  'edit',          # Sorry, but a file should not be editted while debugging, as it made breakpoints shifted
  'play',          # What if the played files or methods include jard again?
  'stat',          # Included in jard UI
  'backtrace',     # Re-implemented later
  'break',         # Re-implemented later
  'exit-all',      # Conflicted with continue
  'exit-program',  # We already have `exit` native command
  '!pry',          # No need to complicate things
  'jump-to',       # No need to complicate things
  'nesting',       # No need to complicate things
  'switch-to',     # No need to complicate things
  'disable-pry'    # No need to complicate things
].freeze
INTERNAL_KEY_BINDINGS =
{
  RubyJard::Keys::CTRL_C => (KEY_BINDING_INTERRUPT = :interrupt)
}.freeze
KEY_READ_TIMEOUT =

200ms

0.2
PTY_OUTPUT_TIMEOUT =

60hz

1.to_f / 60

Instance Method Summary collapse

Constructor Details

#initialize(key_bindings: nil, input: RubyJard::Console.input, output: RubyJard::Console.output) ⇒ ReplProxy

Returns a new instance of ReplProxy.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/ruby_jard/repl_proxy.rb', line 126

def initialize(key_bindings: nil, input: RubyJard::Console.input, output: RubyJard::Console.output)
  @input = input
  @output = output

  @state = ReplState.new

  @pry_input_pipe_read, @pry_input_pipe_write = IO.pipe
  @pry_output_pty_read, @pry_output_pty_write = PTY.open
  @pry = pry_instance

  @key_bindings = key_bindings || RubyJard::KeyBindings.new
  INTERNAL_KEY_BINDINGS.each do |sequence, action|
    @key_bindings.push(sequence, action)
  end

  @pry_pty_output_thread = Thread.new { pry_pty_output }
  @pry_pty_output_thread.name = '<<Jard: Pty Output Thread>>'

  Signal.trap('SIGWINCH') do
    @main_thread.raise FlowInterrupt.new('Resize event', RubyJard::ControlFlow.new(:list))
  end
end

Instance Method Details

#repl(current_binding) ⇒ Object

rubocop:disable Metrics/MethodLength



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/ruby_jard/repl_proxy.rb', line 150

def repl(current_binding)
  @state.ready!
  @openning_pager = false

  RubyJard::Console.disable_echo!(@output)
  RubyJard::Console.raw!(@output)

  # Internally, Pry sneakily updates Readline to global output config
  # when STDOUT is piping regardless of what I pass into Pry instance.
  Pry.config.output = @pry_output_pty_write
  Readline.input = @pry_input_pipe_read
  Readline.output = @pry_output_pty_write
  @pry.binding_stack.clear

  @main_thread = Thread.current

  @pry_input_thread = Thread.new { pry_repl(current_binding) }
  @pry_input_thread.abort_on_exception = true
  @pry_input_thread.report_on_exception = false
  @pry_input_thread.name = '<<Jard: Pry input thread >>'

  @key_listen_thread = Thread.new { listen_key_press }
  @key_listen_thread.abort_on_exception = true
  @key_listen_thread.report_on_exception = false
  @key_listen_thread.name = '<<Jard: Repl key listen >>'

  [@pry_input_thread, @key_listen_thread].map(&:join)
rescue FlowInterrupt => e
  @state.exiting!
  sleep PTY_OUTPUT_TIMEOUT until @state.exited?
  RubyJard::ControlFlow.dispatch(e.flow)
ensure
  RubyJard::Console.enable_echo!(@output)
  RubyJard::Console.cooked!(@output)
  Readline.input = @input
  Readline.output = @output
  Pry.config.output = @output
  @key_listen_thread&.exit if @key_listen_thread&.alive?
  @pry_input_thread&.exit if @pry_input_thread&.alive?
  @state.exited!
end