r_o_v 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []