ruby_jard 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. data/.github/workflows/ruby.yml +85 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +70 -1
  7. data/CHANGELOG.md +31 -0
  8. data/Gemfile +6 -3
  9. data/README.md +122 -8
  10. data/bin/console +1 -2
  11. data/docs/color_schemes/256-light.png +0 -0
  12. data/docs/color_schemes/gruvbox.png +0 -0
  13. data/docs/color_schemes/one-half-dark.png +0 -0
  14. data/docs/color_schemes/one-half-light.png +0 -0
  15. data/lib/ruby_jard.rb +5 -5
  16. data/lib/ruby_jard/box_drawer.rb +4 -1
  17. data/lib/ruby_jard/color_schemes.rb +31 -15
  18. data/lib/ruby_jard/color_schemes/256_color_scheme.rb +37 -37
  19. data/lib/ruby_jard/color_schemes/256_light_color_scheme.rb +62 -0
  20. data/lib/ruby_jard/color_schemes/deep_space_color_scheme.rb +36 -36
  21. data/lib/ruby_jard/color_schemes/gruvbox_color_scheme.rb +62 -0
  22. data/lib/ruby_jard/color_schemes/one_half_dark_color_scheme.rb +61 -0
  23. data/lib/ruby_jard/color_schemes/one_half_light_color_scheme.rb +62 -0
  24. data/lib/ruby_jard/column.rb +3 -1
  25. data/lib/ruby_jard/commands/continue_command.rb +2 -3
  26. data/lib/ruby_jard/commands/down_command.rb +9 -5
  27. data/lib/ruby_jard/commands/exit_command.rb +27 -0
  28. data/lib/ruby_jard/commands/frame_command.rb +11 -10
  29. data/lib/ruby_jard/commands/jard/color_scheme_command.rb +52 -0
  30. data/lib/ruby_jard/commands/jard/hide_command.rb +40 -0
  31. data/lib/ruby_jard/commands/jard/output_command.rb +28 -0
  32. data/lib/ruby_jard/commands/jard/show_command.rb +41 -0
  33. data/lib/ruby_jard/commands/jard_command.rb +50 -0
  34. data/lib/ruby_jard/commands/list_command.rb +5 -4
  35. data/lib/ruby_jard/commands/next_command.rb +10 -5
  36. data/lib/ruby_jard/commands/step_command.rb +10 -5
  37. data/lib/ruby_jard/commands/step_out_command.rb +10 -5
  38. data/lib/ruby_jard/commands/up_command.rb +10 -5
  39. data/lib/ruby_jard/commands/validation_helpers.rb +50 -0
  40. data/lib/ruby_jard/config.rb +7 -3
  41. data/lib/ruby_jard/console.rb +10 -22
  42. data/lib/ruby_jard/control_flow.rb +3 -3
  43. data/lib/ruby_jard/decorators/color_decorator.rb +11 -5
  44. data/lib/ruby_jard/decorators/loc_decorator.rb +1 -1
  45. data/lib/ruby_jard/decorators/path_decorator.rb +20 -7
  46. data/lib/ruby_jard/decorators/source_decorator.rb +2 -0
  47. data/lib/ruby_jard/frame.rb +55 -0
  48. data/lib/ruby_jard/keys.rb +0 -3
  49. data/lib/ruby_jard/layout.rb +9 -2
  50. data/lib/ruby_jard/layout_calculator.rb +29 -12
  51. data/lib/ruby_jard/layout_picker.rb +34 -0
  52. data/lib/ruby_jard/layouts.rb +52 -0
  53. data/lib/ruby_jard/layouts/narrow_horizontal_layout.rb +28 -0
  54. data/lib/ruby_jard/layouts/narrow_vertical_layout.rb +32 -0
  55. data/lib/ruby_jard/layouts/tiny_layout.rb +25 -0
  56. data/lib/ruby_jard/layouts/wide_layout.rb +13 -15
  57. data/lib/ruby_jard/pager.rb +96 -0
  58. data/lib/ruby_jard/repl_processor.rb +61 -31
  59. data/lib/ruby_jard/repl_proxy.rb +193 -89
  60. data/lib/ruby_jard/row.rb +16 -1
  61. data/lib/ruby_jard/row_renderer.rb +51 -42
  62. data/lib/ruby_jard/screen.rb +2 -12
  63. data/lib/ruby_jard/screen_adjuster.rb +104 -0
  64. data/lib/ruby_jard/screen_drawer.rb +3 -0
  65. data/lib/ruby_jard/screen_manager.rb +32 -54
  66. data/lib/ruby_jard/screen_renderer.rb +30 -16
  67. data/lib/ruby_jard/screens.rb +31 -12
  68. data/lib/ruby_jard/screens/backtrace_screen.rb +23 -26
  69. data/lib/ruby_jard/screens/menu_screen.rb +53 -22
  70. data/lib/ruby_jard/screens/source_screen.rb +65 -37
  71. data/lib/ruby_jard/screens/threads_screen.rb +14 -14
  72. data/lib/ruby_jard/screens/variables_screen.rb +59 -34
  73. data/lib/ruby_jard/session.rb +19 -10
  74. data/lib/ruby_jard/span.rb +3 -0
  75. data/lib/ruby_jard/templates/layout_template.rb +1 -1
  76. data/lib/ruby_jard/templates/screen_template.rb +3 -4
  77. data/lib/ruby_jard/version.rb +1 -1
  78. data/ruby_jard.gemspec +1 -1
  79. metadata +38 -9
  80. data/lib/ruby_jard/commands/color_scheme_command.rb +0 -42
  81. data/lib/ruby_jard/layouts/narrow_layout.rb +0 -41
  82. data/lib/ruby_jard/screens/empty_screen.rb +0 -13
@@ -1,14 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ruby_jard/commands/continue_command'
4
- require 'ruby_jard/commands/up_command'
5
- require 'ruby_jard/commands/down_command'
6
- require 'ruby_jard/commands/next_command'
7
- require 'ruby_jard/commands/step_command'
8
- require 'ruby_jard/commands/step_out_command'
9
- require 'ruby_jard/commands/frame_command'
10
- require 'ruby_jard/commands/list_command'
11
- require 'ruby_jard/commands/color_scheme_command'
3
+ require 'pty'
4
+ require 'ruby_jard/pager'
12
5
 
13
6
  module RubyJard
14
7
  ##
@@ -44,7 +37,6 @@ module RubyJard
44
37
  'stat', # Included in jard UI
45
38
  'backtrace', # Re-implemented later
46
39
  'break', # Re-implemented later
47
- 'exit', # Conflicted with continue
48
40
  'exit-all', # Conflicted with continue
49
41
  'exit-program', # We already have `exit` native command
50
42
  '!pry', # No need to complicate things
@@ -54,119 +46,218 @@ module RubyJard
54
46
  'disable-pry' # No need to complicate things
55
47
  ].freeze
56
48
 
57
- COMMANDS = [
58
- CMD_FLOW = :flow,
59
- CMD_EVALUATE = :evaluate,
60
- CMD_IDLE = :idle,
61
- CMD_INTERRUPT = :interrupt
62
- ].freeze
63
-
64
- # rubocop:disable Layout/HashAlignment
65
49
  INTERNAL_KEY_BINDINGS = {
66
- RubyJard::Keys::END_LINE => (KEY_BINDING_ENDLINE = :end_line),
67
- RubyJard::Keys::CTRL_C => (KEY_BINDING_INTERRUPT = :interrupt)
50
+ RubyJard::Keys::CTRL_C => (KEY_BINDING_INTERRUPT = :interrupt)
68
51
  }.freeze
69
- # rubocop:enable Layout/HashAlignment
70
52
 
71
- KEYPRESS_POLLING = 0.1 # 100ms
53
+ KEY_READ_TIMEOUT = 0.2 # 200ms
54
+ PTY_OUTPUT_TIMEOUT = 1.to_f / 60 # 60hz
55
+
56
+ ##
57
+ # A tool to communicate between functional threads and main threads
58
+ class FlowInterrupt < StandardError
59
+ attr_reader :flow
60
+
61
+ def initialize(msg = '', flow = nil)
62
+ super(msg)
63
+ @flow = flow
64
+ end
65
+ end
66
+
67
+ ##
68
+ # A class to store the state with multi-thread guarding
69
+ # Ready => Processing/Exiting
70
+ # Processing => Ready again
71
+ # Exiting => Exited
72
+ # Exited => Ready
73
+ class ReplState
74
+ STATES = [
75
+ STATE_READY = 0,
76
+ STATE_EXITING = 1,
77
+ STATE_PROCESSING = 2,
78
+ STATE_EXITED = 3
79
+ ].freeze
80
+ def initialize
81
+ @state = STATE_EXITED
82
+ @mutex = Mutex.new
83
+ end
84
+
85
+ def check(method_name)
86
+ @mutex.synchronize { yield if send(method_name) }
87
+ end
88
+
89
+ def ready?
90
+ @state == STATE_READY
91
+ end
92
+
93
+ def ready!
94
+ if ready? || processing? || exited?
95
+ @mutex.synchronize { @state = STATE_READY }
96
+ end
97
+ end
98
+
99
+ def processing?
100
+ @state == STATE_PROCESSING
101
+ end
102
+
103
+ def processing!
104
+ return unless ready?
105
+
106
+ @mutex.synchronize { @state = STATE_PROCESSING }
107
+ end
108
+
109
+ def exiting?
110
+ @state == STATE_EXITING
111
+ end
112
+
113
+ def exiting!
114
+ @mutex.synchronize { @state = STATE_EXITING }
115
+ end
116
+
117
+ def exited?
118
+ @state == STATE_EXITED
119
+ end
120
+
121
+ def exited!
122
+ @mutex.synchronize { @state = STATE_EXITED }
123
+ end
124
+ end
72
125
 
73
126
  def initialize(key_bindings: nil)
74
- @pry_read_stream, @pry_write_stream = IO.pipe
127
+ @state = ReplState.new
128
+
129
+ @pry_input_pipe_read, @pry_input_pipe_write = IO.pipe
130
+ @pry_output_pty_read, @pry_output_pty_write = PTY.open
75
131
  @pry = pry_instance
76
- @commands = Queue.new
132
+
77
133
  @key_bindings = key_bindings || RubyJard::KeyBindings.new
78
134
  INTERNAL_KEY_BINDINGS.each do |sequence, action|
79
135
  @key_bindings.push(sequence, action)
80
136
  end
81
- end
82
137
 
83
- def read_key
84
- RubyJard::Console.getch(STDIN, KEYPRESS_POLLING)
138
+ @pry_pty_output_thread = Thread.new { pry_pty_output }
139
+ @pry_pty_output_thread.name = '<<Jard: Pty Output Thread>>'
85
140
  end
86
141
 
87
142
  def repl(current_binding)
88
- Readline.input = @pry_read_stream
89
- @commands.clear
143
+ @state.ready!
144
+ @openning_pager = false
145
+
146
+ RubyJard::Console.disable_echo!
147
+ RubyJard::Console.raw!
148
+
149
+ Readline.input = @pry_input_pipe_read
150
+ Readline.output = @pry_output_pty_write
90
151
  @pry.binding_stack.clear
91
152
 
92
- pry_thread = Thread.new do
93
- pry_repl(current_binding)
94
- end
95
- pry_thread.report_on_exception = false if pry_thread.respond_to?(:report_on_exception)
96
- loop do
97
- break unless pry_thread.alive?
153
+ @main_thread = Thread.current
154
+
155
+ @pry_input_thread = Thread.new { pry_repl(current_binding) }
156
+ @pry_input_thread.abort_on_exception = true
157
+ @pry_input_thread.report_on_exception = false
158
+ @pry_input_thread.name = '<<Jard: Pry input thread >>'
98
159
 
99
- if @commands.empty?
100
- listen_key_press
160
+ @key_listen_thread = Thread.new { listen_key_press }
161
+ @key_listen_thread.abort_on_exception = true
162
+ @key_listen_thread.report_on_exception = false
163
+ @key_listen_thread.name = '<<Jard: Repl key listen >>'
164
+
165
+ [@pry_input_thread, @key_listen_thread].map(&:join)
166
+ rescue FlowInterrupt => e
167
+ @state.exiting!
168
+ sleep PTY_OUTPUT_TIMEOUT until @state.exited?
169
+ RubyJard::ControlFlow.dispatch(e.flow)
170
+ ensure
171
+ RubyJard::Console.enable_echo!
172
+ RubyJard::Console.cooked!
173
+ Readline.input = STDIN
174
+ Readline.output = STDOUT
175
+ @key_listen_thread&.exit if @key_listen_thread&.alive?
176
+ @pry_input_thread&.exit if @pry_input_thread&.alive?
177
+ @state.exited!
178
+ end
179
+
180
+ private
181
+
182
+ def read_key
183
+ RubyJard::Console.getch(STDIN, KEY_READ_TIMEOUT)
184
+ end
185
+
186
+ def pry_pty_output
187
+ loop do
188
+ if @state.exiting?
189
+ if @pry_output_pty_read.ready?
190
+ STDOUT.write @pry_output_pty_read.read_nonblock(2048), from_jard: true
191
+ else
192
+ @state.exited!
193
+ end
194
+ elsif @state.exited?
195
+ sleep PTY_OUTPUT_TIMEOUT
101
196
  else
102
- cmd, value = @commands.deq
103
- handle_command(pry_thread, cmd, value)
197
+ output = @pry_output_pty_read.read_nonblock(2048)
198
+ unless output.nil?
199
+ STDOUT.write output, from_jard: true
200
+ end
104
201
  end
202
+ rescue IO::WaitReadable, IO::WaitWritable
203
+ # Retry
204
+ sleep PTY_OUTPUT_TIMEOUT
105
205
  end
106
- pry_thread&.join
107
- Readline.input = STDIN
108
206
  end
109
207
 
110
208
  def pry_repl(current_binding)
111
209
  flow = RubyJard::ControlFlow.listen do
112
210
  @pry.repl(current_binding)
113
211
  end
114
- @commands << [CMD_FLOW, flow]
115
- rescue StandardError => e
116
- RubyJard::ScreenManager.draw_error(e)
117
- raise
212
+ @state.check(:ready?) do
213
+ @main_thread.raise FlowInterrupt.new('Interrupt from repl thread', flow)
214
+ end
118
215
  end
119
216
 
120
217
  def listen_key_press
121
- key = @key_bindings.match { read_key }
122
- if key.is_a?(RubyJard::KeyBinding)
123
- handle_key_binding(key)
124
- elsif !key.empty?
125
- @pry_write_stream.write(key)
218
+ loop do
219
+ break if @state.exiting? || @state.exited?
220
+
221
+ if @state.processing? && @openning_pager
222
+ # Discard all keys unfortunately
223
+ sleep PTY_OUTPUT_TIMEOUT
224
+ else
225
+ key = @key_bindings.match { read_key }
226
+ if key.is_a?(RubyJard::KeyBinding)
227
+ continue = handle_key_binding(key)
228
+ break unless continue
229
+ elsif !key.empty?
230
+ @pry_input_pipe_write.write(key)
231
+ end
232
+ end
126
233
  end
127
234
  end
128
235
 
129
236
  def handle_key_binding(key_binding)
130
237
  case key_binding.action
131
- when KEY_BINDING_ENDLINE
132
- @pry_write_stream.write(key_binding.sequence)
133
- @commands << [CMD_EVALUATE]
134
238
  when KEY_BINDING_INTERRUPT
135
- @commands << [CMD_INTERRUPT]
239
+ handle_interrupt_command
240
+ true
136
241
  else
137
- @commands << [
138
- CMD_FLOW, RubyJard::ControlFlow.new(:key_binding, action: key_binding.action)
139
- ]
140
- end
141
- end
142
-
143
- def handle_command(pry_thread, cmd, value)
144
- case cmd
145
- when CMD_FLOW
146
- pry_thread.exit if pry_thread.alive?
147
- RubyJard::ControlFlow.dispatch(value)
148
- when CMD_EVALUATE
149
- loop do
150
- cmd, value = @commands.deq
151
- break if [CMD_IDLE, CMD_FLOW, CMD_INTERRUPT].include?(cmd)
242
+ flow = RubyJard::ControlFlow.new(:key_binding, action: key_binding.action)
243
+ @state.check(:ready?) do
244
+ @main_thread.raise FlowInterrupt.new('Interrupt from repl thread', flow)
152
245
  end
153
- handle_command(pry_thread, cmd, value)
154
- when CMD_INTERRUPT
155
- handle_interrupt_command(pry_thread)
156
- when CMD_IDLE
157
- # Ignore
246
+ false
158
247
  end
159
248
  end
160
249
 
161
- def handle_interrupt_command(pry_thread)
162
- pry_thread.raise Interrupt if pry_thread.alive?
250
+ def handle_interrupt_command
251
+ @state.check(:ready?) do
252
+ @pry_input_thread&.raise Interrupt if @pry_input_thread&.alive?
253
+ end
163
254
  loop do
164
255
  begin
165
- sleep 0.1
256
+ sleep PTY_OUTPUT_TIMEOUT
166
257
  rescue Interrupt
167
258
  # Interrupt spam. Ignore.
168
259
  end
169
- break unless pry_thread.pending_interrupt?
260
+ break unless @pry_input_thread&.pending_interrupt?
170
261
  end
171
262
  end
172
263
 
@@ -175,7 +266,8 @@ module RubyJard
175
266
  prompt: pry_jard_prompt,
176
267
  quiet: true,
177
268
  commands: pry_command_set,
178
- hooks: pry_hooks
269
+ hooks: pry_hooks,
270
+ output: @pry_output_pty_write
179
271
  )
180
272
  # I'll be burned in hell for this
181
273
  # TODO: Contact pry author to add :after_handle_line hook
@@ -186,6 +278,10 @@ module RubyJard
186
278
  end
187
279
  alias_method :_original_handle_line, :handle_line
188
280
  alias_method :handle_line, :_jard_handle_line
281
+
282
+ def pager
283
+ RubyJard::Pager.new(self)
284
+ end
189
285
  end
190
286
  pry_instance
191
287
  end
@@ -215,18 +311,26 @@ module RubyJard
215
311
 
216
312
  def pry_hooks
217
313
  hooks = Pry::Hooks.default
218
- hooks.add_hook(:after_read, :jard_proxy_acquire_lock) do |read_string, _pry|
219
- @commands <<
220
- if Pry::Code.complete_expression?(read_string)
221
- [CMD_EVALUATE]
222
- else
223
- [CMD_IDLE]
224
- end
225
- rescue SyntaxError
226
- @commands << [CMD_IDLE]
314
+ hooks.add_hook(:after_read, :jard_proxy_acquire_lock) do |_read_string, _pry|
315
+ RubyJard::Console.cooked!
316
+ @state.processing!
317
+ # Sleep 2 ticks, wait for pry to print out all existing output in the queue
318
+ sleep PTY_OUTPUT_TIMEOUT * 2
227
319
  end
228
320
  hooks.add_hook(:after_handle_line, :jard_proxy_release_lock) do
229
- @commands << [CMD_IDLE]
321
+ RubyJard::Console.raw!
322
+ @state.ready!
323
+ end
324
+ hooks.add_hook(:before_pager, :jard_proxy_before_pager) do
325
+ @openning_pager = true
326
+
327
+ @state.processing!
328
+ RubyJard::Console.cooked!
329
+ end
330
+ hooks.add_hook(:after_pager, :jard_proxy_after_pager) do
331
+ @openning_pager = false
332
+ @state.ready!
333
+ RubyJard::Console.raw!
230
334
  end
231
335
  end
232
336
  end
@@ -1,16 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyJard
4
+ ##
5
+ # This class is an object to store a row of data display on a screen
4
6
  class Row
5
7
  extend Forwardable
6
8
 
7
- attr_accessor :columns, :line_limit, :content
9
+ attr_accessor :columns, :line_limit, :content, :rendered
8
10
 
9
11
  def initialize(line_limit: 1, columns: [], ellipsis: true)
10
12
  @content = []
11
13
  @columns = columns
12
14
  @ellipsis = ellipsis
13
15
  @line_limit = line_limit
16
+ @rendered = false
17
+ end
18
+
19
+ def rendered?
20
+ @rendered == true
21
+ end
22
+
23
+ def mark_rendered
24
+ @rendered = true
25
+ end
26
+
27
+ def reset_rendered
28
+ @rendered = false
14
29
  end
15
30
  end
16
31
  end
@@ -14,63 +14,72 @@ module RubyJard
14
14
  end
15
15
 
16
16
  def render
17
+ @row.reset_rendered
18
+
17
19
  @x = 0
18
20
  @y = 0
21
+ @original_x = 0
22
+ @original_y = 0
19
23
  @content_map = []
20
24
 
21
- original_x = 0
22
- @row.columns.each_with_index do |column, index|
23
- @y = 0
24
- @x = original_x
25
- content_width = column.width
26
- content_width -= 1 if index < @row.columns.length - 1
27
- render_column(column, original_x, content_width)
25
+ @row.columns.each do |column|
26
+ @x = @original_x
27
+ @y = @original_y
28
+
29
+ @drawing_width = 0
30
+ @drawing_lines = 1
28
31
 
29
- original_x += column.width
32
+ column.spans.each do |span|
33
+ render_span(column, span)
34
+ end
35
+
36
+ @original_x += column.width
30
37
  end
31
38
 
32
39
  generate_bitmap
40
+
41
+ @row.mark_rendered
33
42
  end
34
43
 
35
- def render_column(column, original_x, content_width)
36
- width = 0
37
- lines = 1
38
-
39
- column.spans.each do |span|
40
- line_content = span.content
41
-
42
- until line_content.nil? || line_content.empty?
43
- if column.word_wrap == RubyJard::Column::WORD_WRAP_NORMAL
44
- if content_width - width < line_content.length && width != 0
45
- width = 0
46
- lines += 1
47
- @y += 1
48
- @x = original_x
49
- end
50
- elsif column.word_wrap == RubyJard::Column::WORD_WRAP_BREAK_WORD
51
- if content_width - width <= 0
52
- width = 0
53
- lines += 1
54
- @y += 1
55
- @x = original_x
56
- end
57
- elsif content_width - width <= 0
58
- return
44
+ # rubocop:disable Metrics/MethodLength
45
+ def render_span(column, span)
46
+ line_content = span.content
47
+
48
+ until line_content.nil? || line_content.empty?
49
+ if column.word_wrap == RubyJard::Column::WORD_WRAP_NORMAL
50
+ if column.content_width - @drawing_width < line_content.length && @drawing_width != 0
51
+ @drawing_width = 0
52
+ @drawing_lines += 1
53
+ @y += 1
54
+ @x = @original_x
59
55
  end
60
- drawing_content = line_content[0..content_width - width - 1]
61
- line_content = line_content[content_width - width..-1]
62
- width += drawing_content.length
63
-
64
- if !@row.line_limit.nil? && lines >= @row.line_limit && !line_content.nil? && !line_content.empty?
65
- drawing_content[drawing_content.length - ELLIPSIS.length..-1] = ELLIPSIS
66
- draw_content(drawing_content, span.styles)
67
- return
68
- else
69
- draw_content(drawing_content, span.styles)
56
+ elsif column.word_wrap == RubyJard::Column::WORD_WRAP_BREAK_WORD
57
+ if column.content_width - @drawing_width <= 0
58
+ @drawing_width = 0
59
+ @drawing_lines += 1
60
+ @y += 1
61
+ @x = @original_x
70
62
  end
63
+ elsif column.content_width - @drawing_width <= 0
64
+ return
65
+ end
66
+ drawing_content = line_content[0..column.content_width - @drawing_width - 1]
67
+ line_content = line_content[column.content_width - @drawing_width..-1]
68
+ @drawing_width += drawing_content.length
69
+
70
+ if !@row.line_limit.nil? &&
71
+ @drawing_lines >= @row.line_limit &&
72
+ !line_content.nil? &&
73
+ !line_content.empty?
74
+ drawing_content[drawing_content.length - ELLIPSIS.length..-1] = ELLIPSIS
75
+ draw_content(drawing_content, span.styles)
76
+ return
77
+ else
78
+ draw_content(drawing_content, span.styles)
71
79
  end
72
80
  end
73
81
  end
82
+ # rubocop:enable Metrics/MethodLength
74
83
 
75
84
  def draw_content(drawing_content, styles)
76
85
  return if @y < 0 || @y >= @height