Skip to content
This repository was archived by the owner on Mar 12, 2023. It is now read-only.

Commit 4589f39

Browse files
committed
Extract interception logics out to become ReplInterceptor
1 parent 899c771 commit 4589f39

File tree

4 files changed

+205
-132
lines changed

4 files changed

+205
-132
lines changed

lib/ruby_jard.rb

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
require 'ruby_jard/key_bindings'
1616
require 'ruby_jard/repl_processor'
1717
require 'ruby_jard/repl_manager'
18+
require 'ruby_jard/repl_interceptor'
1819
require 'ruby_jard/repl_state'
1920
require 'ruby_jard/screen_manager'
2021
require 'ruby_jard/reflection'

lib/ruby_jard/pry_proxy.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ def repl(target = nil)
7373
OriginalReadline.output = @redirected_output
7474
PryReplProxy.new(self, target: target).start
7575
ensure
76-
Readline.input = @original_input
77-
Readline.output = @original_output
76+
OriginalReadline.input = @original_input
77+
OriginalReadline.output = @original_output
7878
end
7979

8080
def pager

lib/ruby_jard/repl_interceptor.rb

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# frozen_string_literal: true
2+
3+
module RubyJard
4+
class ReplInterceptor
5+
INTERNAL_KEY_BINDINGS = {
6+
RubyJard::Keys::CTRL_C => (KEY_BINDING_INTERRUPT = :interrupt)
7+
}.freeze
8+
9+
KEY_READ_TIMEOUT = 0.2 # 200ms
10+
PTY_OUTPUT_TIMEOUT = 1.to_f / 60 # 60hz
11+
12+
def initialize(state, console, key_bindings)
13+
@state = state
14+
@console = console
15+
16+
@key_bindings = key_bindings || RubyJard::KeyBindings.new
17+
INTERNAL_KEY_BINDINGS.each do |sequence, action|
18+
@key_bindings.push(sequence, action)
19+
end
20+
21+
reopen_streams
22+
start_output_bridge
23+
end
24+
25+
def start
26+
reopen_streams
27+
start_key_listen_thread
28+
end
29+
30+
def stop
31+
@key_listen_thread&.exit if @key_listen_thread&.alive?
32+
end
33+
34+
def dispatch_command(command)
35+
@input_writer.write("#{RubyJard::ReplManager::COMMAND_ESCAPE_SEQUENCE}#{command}\n")
36+
end
37+
38+
def feed_output(content)
39+
@output_writer.write(content)
40+
end
41+
42+
def feed_input(content)
43+
@input_writer.write(content)
44+
rescue IOError
45+
# Nothing to do. Discard the content
46+
end
47+
48+
def original_input
49+
@console.input
50+
end
51+
52+
def original_output
53+
@console.output
54+
end
55+
56+
def redirected_input
57+
@input_reader
58+
end
59+
60+
def redirected_output
61+
@output_writer
62+
end
63+
64+
def redirectable?
65+
true
66+
end
67+
68+
private
69+
70+
def reopen_streams
71+
unless redirectable?
72+
@input_reader = @console.input
73+
@input_writer = @console.input
74+
@output_reader = @console.output
75+
@output_writer = @console.output
76+
return
77+
end
78+
79+
if !defined?(@input_reader) || @input_reader.closed? || @input_writer.closed?
80+
@input_reader, @input_writer = IO.pipe
81+
end
82+
83+
if !defined?(@output_reader) || @output_reader.closed? || @output_writer.closed?
84+
@output_reader, @output_writer = PTY.open
85+
end
86+
end
87+
88+
def start_output_bridge
89+
return unless redirectable?
90+
91+
@output_bridge_thread = Thread.new { output_bridge }
92+
@output_bridge_thread.abort_on_exception = true
93+
@output_bridge_thread.report_on_exception = false
94+
@output_bridge_thread.name = '<<Jard: Pty Output Thread>>'
95+
end
96+
97+
def start_key_listen_thread
98+
return unless redirectable?
99+
100+
@main_thread = Thread.current
101+
@key_listen_thread = Thread.new { listen_key_press }
102+
@key_listen_thread.abort_on_exception = true
103+
@key_listen_thread.report_on_exception = false
104+
@key_listen_thread.name = '<<Jard: Repl key listen >>'
105+
end
106+
107+
def output_bridge
108+
loop do
109+
if @state.exiting?
110+
if @output_reader.ready?
111+
write_output(@output_reader.read_nonblock(2048))
112+
else
113+
@state.exited!
114+
end
115+
elsif @state.exited?
116+
sleep PTY_OUTPUT_TIMEOUT
117+
else
118+
content = @output_reader.read_nonblock(2048)
119+
unless content.nil?
120+
write_output(content)
121+
end
122+
end
123+
rescue IO::WaitReadable, IO::WaitWritable
124+
# Retry
125+
sleep PTY_OUTPUT_TIMEOUT
126+
end
127+
rescue StandardError
128+
# This thread shoud never die, or the user may be freezed, and cannot type anything
129+
sleep 0.5
130+
retry
131+
end
132+
133+
def listen_key_press
134+
loop do
135+
break if @input_writer.closed?
136+
break if @state.exiting? || @state.exited?
137+
138+
if @state.processing? && @state.pager?
139+
# Discard all keys unfortunately
140+
sleep PTY_OUTPUT_TIMEOUT
141+
else
142+
key = @key_bindings.match { @console.getch(KEY_READ_TIMEOUT) }
143+
if key.is_a?(RubyJard::KeyBinding)
144+
handle_key_binding(key)
145+
elsif !key.empty?
146+
@input_writer.write(key)
147+
end
148+
end
149+
end
150+
rescue StandardError => e
151+
# This thread shoud never die, or the user may be freezed, and cannot type anything
152+
sleep 0.5
153+
RubyJard.debug(e.inspect)
154+
retry
155+
end
156+
157+
def handle_key_binding(key_binding)
158+
case key_binding.action
159+
when KEY_BINDING_INTERRUPT
160+
handle_interrupt_command
161+
else
162+
@state.check(:ready?) do
163+
dispatch_command(key_binding.action)
164+
end
165+
end
166+
end
167+
168+
def handle_interrupt_command
169+
@state.check(:ready?) do
170+
@main_thread&.raise Interrupt if @main_thread&.alive?
171+
end
172+
loop do
173+
begin
174+
sleep PTY_OUTPUT_TIMEOUT
175+
rescue Interrupt
176+
# Interrupt spam. Ignore.
177+
end
178+
break unless @main_thread&.pending_interrupt?
179+
end
180+
end
181+
182+
def write_output(content)
183+
return if content.include?(RubyJard::ReplManager::COMMAND_ESCAPE_SEQUENCE)
184+
185+
@console.write content.force_encoding('UTF-8')
186+
end
187+
end
188+
end

0 commit comments

Comments
 (0)