r_o_v 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/r_o_v.rb +541 -0
  3. metadata +57 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ea03b50c9ce97d3300f76bf892de2bc9b3c1d04d1424066a47921c55363fbe1
4
+ data.tar.gz: 10bd393dae1fab92df73a5788e0d3fa60ec799ae97eeb2fc9aef2703aeef310f
5
+ SHA512:
6
+ metadata.gz: ab6da8547bd0e72dbf5021e2b74f6b28c5fc06ce34ee2674deb4b599eb4ebc643f0124c7961e278644ad75161d626c6ea43740ea5159fb8383e5ab2683d109d1
7
+ data.tar.gz: b815fcad8ad483256f2dee3cf4be668fbc58f8d28ddfc527196f8c3e790dda906527939a8f40edada37c30afff94ace2a46723ae309e490abfcb1463a8fdd438
data/lib/r_o_v.rb ADDED
@@ -0,0 +1,541 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO:
4
+ # - close parent should work on the child too (go to parent and then close)
5
+ # - sluggishness on M1 + Rails + pry
6
+ # - fuzzy search - jump
7
+ # - parallel open (same trail)
8
+ # - memory slabs
9
+
10
+ class ROV
11
+ class Util
12
+ class << self
13
+ def red(s); escape(s, 91); end
14
+ def green(s); escape(s, 92); end
15
+ def yellow(s); escape(s, 93); end
16
+ def blue(s); escape(s, 94); end
17
+ def magenta(s); escape(s, 95); end
18
+ def cyan(s); escape(s, 96); end
19
+ def bold(s); escape(s, 1); end
20
+ def dim(s); escape(s, 22); end
21
+ def invert(s); escape(s, 7); end
22
+ def console_lines; %x`tput lines`.to_i; end
23
+ def console_cols; %x`tput cols`.to_i; end
24
+
25
+ def visible_truncate(s, lim)
26
+ return s unless s.size > lim # Quick escape to save gsub use.
27
+ return s unless visible_str_len(s) > lim
28
+
29
+ vis_count = 0
30
+ full_count = nil
31
+ in_escape = false
32
+
33
+
34
+ s.chars.each_with_index do |c, idx|
35
+ if in_escape
36
+ in_escape = false if c == 'm'
37
+ else
38
+ if c == "\e"
39
+ in_escape = true
40
+ else
41
+ vis_count += 1
42
+
43
+ if vis_count >= lim - 1
44
+ full_count = idx
45
+ break
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ s[..full_count] + "\x1B[0m…"
52
+ end
53
+
54
+ def visible_str_len(str); str.gsub(/\e\[\d+m/, '').size; end
55
+
56
+ def simple_type?(o)
57
+ case o
58
+ when String, Numeric, Symbol, TrueClass, FalseClass, NilClass then true
59
+ else false
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def escape(s, color_code); "\x1B[#{color_code}m#{s}\x1B[0m"; end
66
+ end
67
+ end
68
+
69
+ class Ctx
70
+ attr_reader(:parent_ctx)
71
+ attr_reader(:children_ctx)
72
+ attr_reader(:tag)
73
+ attr_reader(:current_level)
74
+ attr_reader(:obj)
75
+ attr_reader(:selection)
76
+
77
+ def initialize(obj, parent_ctx, current_level:)
78
+ @obj = obj
79
+ @parent_ctx = parent_ctx
80
+ @selection = children_size > 0 ? 0 : nil
81
+ @children_ctx = [nil] * children_size
82
+ @tag = obj.class.name
83
+ @current_level = current_level
84
+ end
85
+
86
+ def select_next
87
+ return unless children_size > 0
88
+ self.selection = (selection + 1) % children_size
89
+ end
90
+
91
+ def select_prev
92
+ return unless children_size > 0
93
+ self.selection = (selection - 1) % children_size
94
+ end
95
+
96
+ def select_first
97
+ self.selection = 0
98
+ end
99
+
100
+ def select_last
101
+ self.selection = children_size - 1
102
+ end
103
+
104
+ def at_last_child?
105
+ selection == children_size - 1
106
+ end
107
+
108
+ def at_first_child?
109
+ selection == 0
110
+ end
111
+
112
+ def children_size
113
+ @children_size ||= case obj
114
+ when Enumerable
115
+ obj.respond_to?(:size) ? obj.size : obj.to_a.size
116
+ # TODO Maybe this can coexist with enumerable (eg sg that fakes enumarable).
117
+ else
118
+ obj.instance_variables.size
119
+ end
120
+ end
121
+
122
+ def has_children?
123
+ children_size > 0
124
+ end
125
+
126
+ def children_names
127
+ @children_names ||= case obj
128
+ when Hash then obj.keys
129
+ when Enumerable then children_size.times.to_a
130
+ else obj.instance_variables
131
+ end
132
+ end
133
+
134
+ #
135
+ # Actual object children (raw object).
136
+ #
137
+ def child_at(index)
138
+ case obj
139
+ when Hash then obj.values[index]
140
+ when Enumerable then obj.to_a[index]
141
+ else obj.instance_variable_get(obj.instance_variables[index])
142
+ end
143
+ end
144
+
145
+ def active_child
146
+ return if selection.nil?
147
+
148
+ raise("Selection must be positive") unless selection >= 0
149
+ raise("Selection is out of bounds") unless selection < children_size
150
+
151
+ child_at(selection)
152
+ end
153
+
154
+ def active_child_var_name
155
+ case obj
156
+ when Hash
157
+ key = obj.keys[selection]
158
+
159
+ if Util.simple_type?(key)
160
+ "[#{key.inspect.gsub('"', '\'')}]"
161
+ else
162
+ ".values[#{selection}]"
163
+ end
164
+ when Enumerable
165
+ "[#{selection}]"
166
+ else
167
+ ".#{obj.instance_variables[selection][1..-1]}"
168
+ end
169
+ end
170
+
171
+ def active_child_open?
172
+ !children_ctx[selection].nil?
173
+ end
174
+
175
+ def child_openable?(index)
176
+ case (elem = child_at(index))
177
+ when IO then elem.instance_variables.size > 0
178
+ when Enumerable then elem.to_a.size > 0
179
+ else elem.instance_variables.size > 0
180
+ end
181
+ end
182
+
183
+ def active_child_openable?
184
+ child_openable?(selection)
185
+ end
186
+
187
+ def open_active_child
188
+ raise("Child is not openable") unless active_child_openable?
189
+ children_ctx[selection] ||= Ctx.new(active_child, self, current_level: current_level + 1)
190
+ end
191
+
192
+ def open_nth_child(idx)
193
+ raise("Child is not openable") unless child_openable?(idx)
194
+ children_ctx[idx] ||= Ctx.new(child_at(idx), self, current_level: current_level + 1)
195
+ end
196
+
197
+ def open_children
198
+ children_size.times do |i|
199
+ next unless children_ctx[i].nil?
200
+ next unless child_openable?(i)
201
+
202
+ children_ctx[i] = Ctx.new(child_at(i), self, current_level: current_level + 1)
203
+ end
204
+ end
205
+
206
+ # TODO: Lets not lose the object, lets have a prop for closed.
207
+ def close_active_child
208
+ children_ctx[selection] = nil
209
+ end
210
+
211
+ def close_children
212
+ children_size.times { |i| children_ctx[i] = nil }
213
+ end
214
+
215
+ #
216
+ # @return [String, Boolean] = [Output, Is-cursor-line?]
217
+ #
218
+ def pretty_print(active_ctx, indent = ' ')
219
+ out = []
220
+
221
+ children_names.zip(children_ctx).each_with_index do |(elem_name, child_ctx), index|
222
+ value_suffix = child_openable?(index) ? '' : " = #{Util.cyan(child_at(index).to_s)}"
223
+ tag_suffix = " (#{Util.magenta(child_at(index).class.name)})#{value_suffix}"
224
+
225
+ is_active_line = self == active_ctx && selection == index
226
+ active_pos_marker = is_active_line ? '>' : ' '
227
+ nesting_symbol = index == children_size - 1 ? '└' : '├'
228
+ tree_more_symbol = child_openable?(index) ? '+ ': ' '
229
+
230
+ line = <<~LINE.lines(chomp: true).join
231
+ #{indent}
232
+ #{Util.bold(Util.yellow(active_pos_marker))}
233
+ #{nesting_symbol}─
234
+ #{tree_more_symbol}
235
+ #{is_active_line ? Util.invert(Util.blue(elem_name)) : Util.blue(elem_name)}
236
+ #{tag_suffix}
237
+ LINE
238
+
239
+ out << [line, is_active_line]
240
+
241
+ if child_ctx
242
+ tree_guide = index == children_size - 1 ? ' ' : '¦'
243
+ out += child_ctx.pretty_print(active_ctx, indent + " #{tree_guide}")
244
+ end
245
+ end
246
+
247
+ out
248
+ end
249
+
250
+ def is_list
251
+ @obj.is_a?(Enumerable)
252
+ end
253
+
254
+ private
255
+
256
+ def children_ctx=
257
+ raise
258
+ end
259
+
260
+ def parent_ctx=
261
+ raise
262
+ end
263
+
264
+ def obj=
265
+ raise
266
+ end
267
+
268
+ attr_writer(:selection)
269
+ end
270
+
271
+ class << self
272
+ def [](obj)
273
+ ROV.new(obj).loop
274
+ end
275
+ end
276
+
277
+ def initialize(obj)
278
+ @root_ctx = @active_ctx = Ctx.new(obj, nil, current_level: 0)
279
+ @is_running = true
280
+ @terminal_width = Util.console_cols
281
+ @variable_name = get_input_presentation
282
+ end
283
+
284
+ def loop
285
+ return unless root_ctx.has_children?
286
+
287
+ while @is_running
288
+ print_root
289
+ execute(read_char)
290
+ end
291
+
292
+ current_variable_as_expression
293
+ end
294
+
295
+ def execute(input)
296
+ case input
297
+ when 'q' then stop_loop
298
+ when 'w' then step_up
299
+ when 's' then step_down
300
+ when 'a' then step_parent
301
+ when 'd' then step_child
302
+ when 'h' then step_home
303
+ when 'e' then close_active_child
304
+ when '0'..'9' then open_tree_level(input.to_i)
305
+ when 'i' then idbg_ext_log
306
+ when 'p' then open_parallel_children
307
+ end
308
+ end
309
+
310
+ def current_variable_as_expression
311
+ @variable_name + active_var_path
312
+ end
313
+
314
+ private
315
+
316
+ attr_reader :root_ctx
317
+ attr_accessor :active_ctx
318
+
319
+ def root_ctx=
320
+ raise
321
+ end
322
+
323
+ #
324
+ # Quit.
325
+ #
326
+ def stop_loop
327
+ @is_running = false
328
+ end
329
+
330
+ #
331
+ # Set current CTX to the parent.
332
+ #
333
+ def step_parent
334
+ self.active_ctx = active_ctx.parent_ctx unless active_ctx.parent_ctx.nil?
335
+ end
336
+
337
+ #
338
+ # Set current CTX to active child.
339
+ #
340
+ def step_child
341
+ self.active_ctx = active_ctx.open_active_child if active_ctx.active_child_openable?
342
+ end
343
+
344
+ #
345
+ # Set current CTX to root.
346
+ #
347
+ def step_home
348
+ self.active_ctx = root_ctx
349
+ active_ctx.select_first
350
+ end
351
+
352
+ #
353
+ # Clost current CTX active child.
354
+ #
355
+ def close_active_child
356
+ active_ctx.close_active_child
357
+ end
358
+
359
+ #
360
+ # Set active CTX to previous child or previous opened subtree last leaf.
361
+ #
362
+ def step_up
363
+ if active_ctx.at_first_child?
364
+ if active_ctx.parent_ctx
365
+ self.active_ctx = active_ctx.parent_ctx
366
+ else
367
+ active_ctx.select_last
368
+
369
+ while active_ctx.active_child_open?
370
+ self.active_ctx = active_ctx.open_active_child
371
+ active_ctx.select_last
372
+ end
373
+ end
374
+ return
375
+ end
376
+
377
+ active_ctx.select_prev
378
+
379
+ while active_ctx.active_child_open?
380
+ self.active_ctx = active_ctx.open_active_child
381
+ active_ctx.select_last
382
+ end
383
+ end
384
+
385
+ #
386
+ # Set active CTX to next child or next opened subtree first node.
387
+ #
388
+ def step_down
389
+ if active_ctx.active_child_open?
390
+ self.active_ctx = active_ctx.open_active_child
391
+ active_ctx.select_first
392
+ return
393
+ end
394
+
395
+ unless active_ctx.at_last_child?
396
+ active_ctx.select_next
397
+ return
398
+ end
399
+
400
+ while active_ctx.at_last_child? && active_ctx.parent_ctx
401
+ self.active_ctx = active_ctx.parent_ctx
402
+ end
403
+
404
+ active_ctx.select_next
405
+ end
406
+
407
+ #
408
+ # Open all nodes on level N.
409
+ #
410
+ def open_tree_level(n)
411
+ while n < active_ctx.current_level
412
+ self.active_ctx = active_ctx.parent_ctx
413
+ end
414
+
415
+ open_tree_level_until(root_ctx, n)
416
+ end
417
+
418
+ def open_tree_level_until(ctx, n)
419
+ if n == 0
420
+ ctx.close_children
421
+ return
422
+ end
423
+
424
+ ctx.open_children
425
+ ctx.children_ctx.each do |child_ctx|
426
+ next unless child_ctx
427
+
428
+ open_tree_level_until(child_ctx, n - 1)
429
+ end
430
+ end
431
+
432
+ def idbg_ext_log
433
+ return unless Object.const_defined?("IDbg")
434
+ IDbg.log("ROV", current_variable_as_expression, active_ctx.active_child)
435
+ end
436
+
437
+ def open_parallel_children
438
+ # Find first enumerable parent.
439
+ enumerable_parent_ctx = active_ctx
440
+ trail = []
441
+
442
+ while enumerable_parent_ctx
443
+ break if enumerable_parent_ctx.is_list
444
+
445
+ trail.unshift(enumerable_parent_ctx.selection)
446
+
447
+ enumerable_parent_ctx = enumerable_parent_ctx.parent_ctx
448
+ end
449
+ return unless enumerable_parent_ctx
450
+
451
+ # Set expected type.
452
+ expected_class = enumerable_parent_ctx.active_child.class
453
+
454
+ # Check if all child is the same.
455
+ is_uniform = enumerable_parent_ctx.obj.to_a.all? { |e| e.is_a?(expected_class) }
456
+
457
+ return unless is_uniform
458
+
459
+ enumerable_parent_ctx.children_size.times do |i|
460
+ next unless enumerable_parent_ctx.child_openable?(i)
461
+
462
+ trail_ctx = enumerable_parent_ctx.open_nth_child(i)
463
+
464
+ # trail.shift # First item is the current iteration.
465
+ trail.each do |child_idx|
466
+ break unless trail_ctx.child_openable?(child_idx)
467
+
468
+ trail_ctx = trail_ctx.open_nth_child(child_idx)
469
+ end
470
+ end
471
+ end
472
+
473
+ def active_var_path
474
+ current_ctx = active_ctx
475
+ path = []
476
+ while current_ctx
477
+ path.unshift(current_ctx.active_child_var_name)
478
+ current_ctx = current_ctx.parent_ctx
479
+ end
480
+
481
+ path.join
482
+ end
483
+
484
+ def print_root
485
+ clear_terminal
486
+
487
+ lines = [[Util.magenta(root_ctx.tag) + ":", false]]
488
+ lines += root_ctx.pretty_print(active_ctx)
489
+ active_line_index = lines.index { |_, is_active| is_active }
490
+
491
+ puts (lines[presentable_line_range(active_line_index, lines.size)].map do |line, _|
492
+ Util.visible_truncate(line, @terminal_width)
493
+ end.join("\n"))
494
+ puts "\n📋 #{@variable_name}#{Util.green(active_var_path)}"
495
+ end
496
+
497
+ def presentable_line_range(mid_index, len)
498
+ padding = (Util.console_lines - 4) / 2
499
+
500
+ if mid_index <= padding
501
+ from = 0
502
+ to = [padding * 2 + 1, len - 1].min
503
+ elsif mid_index + padding >= len
504
+ to = len - 1
505
+ from = [0, to - 1 - 2 * padding].max
506
+ else
507
+ from = [0, mid_index - padding - 1].max
508
+ to = [mid_index + padding, len].min
509
+ end
510
+
511
+ from..to
512
+ end
513
+
514
+ def clear_terminal
515
+ print `clear`
516
+ end
517
+
518
+ def read_char
519
+ system('stty', 'raw', '-echo')
520
+ char = STDIN.getc
521
+ system('stty', '-raw', 'echo')
522
+ char
523
+ end
524
+
525
+ def get_input_presentation
526
+ rx = /ROV\[(?<varname>.+)\]$/
527
+
528
+ if Object.const_defined?("Pry")
529
+ last_pry_call = Pry.history.to_a.last
530
+ return rx.match(last_pry_call.rstrip)["varname"]
531
+ elsif Object.const_defined?("IRB")
532
+ IRB.CurrentContext.io.save_history
533
+ last_irb_call = IO.readlines(File.expand_path(IRB.rc_file("_history"))).last
534
+ return rx.match(last_irb_call.rstrip)["varname"]
535
+ end
536
+
537
+ "_"
538
+ rescue
539
+ "-"
540
+ end
541
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: r_o_v
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - itarato
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-08-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description:
28
+ email: it.arato@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/r_o_v.rb
34
+ homepage: https://github.com/itarato/Ruby-Object-Viewer/
35
+ licenses:
36
+ - GPL-3.0-or-later
37
+ metadata: {}
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.0.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.4.10
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Tree style Ruby object viewer (for the terminal)
57
+ test_files: []