Class: RubyJard::ReplProxy
- Inherits:
-
Object
- Object
- RubyJard::ReplProxy
- 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
-
#initialize(key_bindings: nil, input: RubyJard::Console.input, output: RubyJard::Console.output) ⇒ ReplProxy
constructor
A new instance of ReplProxy.
-
#repl(current_binding) ⇒ Object
rubocop:disable Metrics/MethodLength.
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 |