scryglass 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.tool-versions +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +33 -0
- data/LICENSE.txt +21 -0
- data/README.md +252 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/example_config.rb +30 -0
- data/lib/example_material.rb +97 -0
- data/lib/hexes.rb +135 -0
- data/lib/prog.rb +89 -0
- data/lib/refinements/ansiless_string_refinement.rb +11 -0
- data/lib/refinements/array_fit_to_refinement.rb +67 -0
- data/lib/refinements/clip_string_refinement.rb +27 -0
- data/lib/refinements/constant_defined_string_refinement.rb +11 -0
- data/lib/scryglass.rb +177 -0
- data/lib/scryglass/config.rb +103 -0
- data/lib/scryglass/lens_helper.rb +22 -0
- data/lib/scryglass/lens_panel.rb +140 -0
- data/lib/scryglass/ro.rb +237 -0
- data/lib/scryglass/ro_builder.rb +402 -0
- data/lib/scryglass/session.rb +514 -0
- data/lib/scryglass/tree_panel.rb +122 -0
- data/lib/scryglass/version.rb +3 -0
- data/lib/scryglass/view_panel.rb +91 -0
- data/lib/scryglass/view_wrapper.rb +23 -0
- data/scryglass.gemspec +46 -0
- metadata +117 -0
@@ -0,0 +1,402 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Scryglass
|
4
|
+
module RoBuilder
|
5
|
+
private
|
6
|
+
|
7
|
+
def roify(object,
|
8
|
+
parent_ro:,
|
9
|
+
key: nil,
|
10
|
+
key_value_relationship_indicator: false,
|
11
|
+
special_sub_ro_type: nil,
|
12
|
+
depth:)
|
13
|
+
|
14
|
+
given_ro_params = {
|
15
|
+
key: key,
|
16
|
+
key_value_relationship_indicator: key_value_relationship_indicator,
|
17
|
+
special_sub_ro_type: special_sub_ro_type,
|
18
|
+
parent_ro: parent_ro,
|
19
|
+
depth: depth
|
20
|
+
}
|
21
|
+
|
22
|
+
object_is_an_activerecord_enum = %w[ActiveRecord_Relation
|
23
|
+
ActiveRecord_Associations_CollectionProxy]
|
24
|
+
.include?(object.class.to_s.split('::').last)
|
25
|
+
ro =
|
26
|
+
if object.class == Hash
|
27
|
+
roify_hash(object, **given_ro_params)
|
28
|
+
elsif object.class == Array
|
29
|
+
roify_array(object, **given_ro_params)
|
30
|
+
elsif object_is_an_activerecord_enum
|
31
|
+
roify_ar_relation(object, **given_ro_params)
|
32
|
+
else
|
33
|
+
Scryglass::Ro.new(
|
34
|
+
scry_session: self,
|
35
|
+
val: object,
|
36
|
+
val_type: :nugget,
|
37
|
+
**given_ro_params
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
ro
|
42
|
+
end
|
43
|
+
|
44
|
+
def roify_array(array,
|
45
|
+
key:,
|
46
|
+
key_value_relationship_indicator:,
|
47
|
+
parent_ro:,
|
48
|
+
special_sub_ro_type: nil,
|
49
|
+
depth:)
|
50
|
+
new_ro = Scryglass::Ro.new(
|
51
|
+
scry_session: self,
|
52
|
+
val: array,
|
53
|
+
val_type: :bucket,
|
54
|
+
key: key,
|
55
|
+
key_value_relationship_indicator: key_value_relationship_indicator,
|
56
|
+
special_sub_ro_type: special_sub_ro_type,
|
57
|
+
parent_ro: parent_ro,
|
58
|
+
depth: depth
|
59
|
+
)
|
60
|
+
return new_ro if array.empty?
|
61
|
+
|
62
|
+
task = Prog::Task.new(max_count: array.count)
|
63
|
+
progress_bar << task
|
64
|
+
|
65
|
+
array.each do |o|
|
66
|
+
new_ro.sub_ros << roify(o, parent_ro: new_ro, depth: depth + 1)
|
67
|
+
task.tick
|
68
|
+
print_progress_bar
|
69
|
+
end
|
70
|
+
|
71
|
+
new_ro
|
72
|
+
end
|
73
|
+
|
74
|
+
def roify_ar_relation(ar_relation,
|
75
|
+
key:,
|
76
|
+
key_value_relationship_indicator:,
|
77
|
+
special_sub_ro_type: nil,
|
78
|
+
parent_ro:,
|
79
|
+
depth:)
|
80
|
+
new_ro = Scryglass::Ro.new(
|
81
|
+
scry_session: self,
|
82
|
+
val: ar_relation,
|
83
|
+
val_type: :bucket,
|
84
|
+
key: key,
|
85
|
+
key_value_relationship_indicator: key_value_relationship_indicator,
|
86
|
+
special_sub_ro_type: special_sub_ro_type,
|
87
|
+
parent_ro: parent_ro,
|
88
|
+
depth: depth
|
89
|
+
)
|
90
|
+
return new_ro if ar_relation.empty?
|
91
|
+
|
92
|
+
task = Prog::Task.new(max_count: ar_relation.count)
|
93
|
+
progress_bar << task
|
94
|
+
|
95
|
+
ar_relation.each do |o|
|
96
|
+
new_ro.sub_ros << roify(o, parent_ro: new_ro, depth: depth + 1)
|
97
|
+
task.tick
|
98
|
+
print_progress_bar
|
99
|
+
end
|
100
|
+
|
101
|
+
new_ro
|
102
|
+
end
|
103
|
+
|
104
|
+
def roify_hash(hash,
|
105
|
+
key:,
|
106
|
+
key_value_relationship_indicator:,
|
107
|
+
special_sub_ro_type: nil,
|
108
|
+
parent_ro:,
|
109
|
+
depth:)
|
110
|
+
new_ro = Scryglass::Ro.new(
|
111
|
+
scry_session: self,
|
112
|
+
val: hash,
|
113
|
+
val_type: :bucket,
|
114
|
+
key: key,
|
115
|
+
key_value_relationship_indicator: key_value_relationship_indicator,
|
116
|
+
special_sub_ro_type: special_sub_ro_type,
|
117
|
+
parent_ro: parent_ro,
|
118
|
+
depth: depth
|
119
|
+
)
|
120
|
+
return new_ro if hash.empty?
|
121
|
+
|
122
|
+
task = Prog::Task.new(max_count: hash.count)
|
123
|
+
progress_bar << task
|
124
|
+
|
125
|
+
hash.each do |k, v|
|
126
|
+
new_ro.sub_ros << roify(v,
|
127
|
+
parent_ro: new_ro,
|
128
|
+
key: k,
|
129
|
+
key_value_relationship_indicator: ' => ',
|
130
|
+
depth: depth + 1)
|
131
|
+
task.tick
|
132
|
+
print_progress_bar
|
133
|
+
end
|
134
|
+
|
135
|
+
new_ro
|
136
|
+
end
|
137
|
+
|
138
|
+
def build_instance_variables_for_target_ros
|
139
|
+
original_ro_total = all_ros.count
|
140
|
+
|
141
|
+
if special_command_targets.any?
|
142
|
+
task = Prog::Task.new(max_count: special_command_targets.count)
|
143
|
+
progress_bar << task
|
144
|
+
|
145
|
+
target_ros = special_command_targets.dup # dup because some commands
|
146
|
+
# create ros which are added to all_ros and then this process starts
|
147
|
+
# adding them to the list of things it tries to act on!
|
148
|
+
target_ros.each do |target_ro|
|
149
|
+
build_iv_sub_ros_for(target_ro)
|
150
|
+
task.tick
|
151
|
+
print_progress_bar
|
152
|
+
end
|
153
|
+
self.special_command_targets = []
|
154
|
+
else
|
155
|
+
build_iv_sub_ros_for(current_ro)
|
156
|
+
expand!(current_ro) if current_ro.iv_sub_ros.any?
|
157
|
+
end
|
158
|
+
|
159
|
+
new_ro_total = all_ros.count
|
160
|
+
recalculate_indeces unless new_ro_total == original_ro_total
|
161
|
+
end
|
162
|
+
|
163
|
+
def build_activerecord_relations_for_target_ros
|
164
|
+
original_ro_total = all_ros.count
|
165
|
+
|
166
|
+
if special_command_targets.any?
|
167
|
+
task = Prog::Task.new(max_count: special_command_targets.count)
|
168
|
+
progress_bar << task
|
169
|
+
|
170
|
+
target_ros = special_command_targets.dup # dup because some commands
|
171
|
+
# create ros which are added to all_ros and then this process starts
|
172
|
+
# adding them to the list of things it tries to act on!
|
173
|
+
target_ros.each do |target_ro|
|
174
|
+
build_ar_sub_ros_for(target_ro)
|
175
|
+
task.tick
|
176
|
+
print_progress_bar
|
177
|
+
end
|
178
|
+
self.special_command_targets = []
|
179
|
+
else
|
180
|
+
build_ar_sub_ros_for(current_ro)
|
181
|
+
expand!(current_ro) if current_ro.ar_sub_ros.any?
|
182
|
+
end
|
183
|
+
new_ro_total = all_ros.count
|
184
|
+
|
185
|
+
recalculate_indeces unless new_ro_total == original_ro_total
|
186
|
+
end
|
187
|
+
|
188
|
+
def build_enum_children_for_target_ros
|
189
|
+
original_ro_total = all_ros.count
|
190
|
+
|
191
|
+
if special_command_targets.any?
|
192
|
+
task = Prog::Task.new(max_count: special_command_targets.count)
|
193
|
+
progress_bar << task
|
194
|
+
|
195
|
+
target_ros = special_command_targets.dup # dup because some commands
|
196
|
+
# create ros which are added to all_ros and then this process starts
|
197
|
+
# adding them to the list of things it tries to act on!
|
198
|
+
target_ros.each do |target_ro|
|
199
|
+
build_enum_children_for(target_ro)
|
200
|
+
task.tick
|
201
|
+
print_progress_bar
|
202
|
+
end
|
203
|
+
self.special_command_targets = []
|
204
|
+
else
|
205
|
+
build_enum_children_for(current_ro)
|
206
|
+
expand!(current_ro) if current_ro.enum_sub_ros.any?
|
207
|
+
end
|
208
|
+
|
209
|
+
new_ro_total = all_ros.count
|
210
|
+
recalculate_indeces unless new_ro_total == original_ro_total
|
211
|
+
end
|
212
|
+
|
213
|
+
def recalculate_indeces
|
214
|
+
all_ordered_ros = []
|
215
|
+
scanning_ro = top_ro
|
216
|
+
|
217
|
+
task = Prog::Task.new(max_count: all_ros.count)
|
218
|
+
progress_bar << task
|
219
|
+
|
220
|
+
while scanning_ro
|
221
|
+
all_ordered_ros << scanning_ro
|
222
|
+
next_ro = scanning_ro.next_ro_without_using_index
|
223
|
+
scanning_ro = next_ro
|
224
|
+
task.tick
|
225
|
+
print_progress_bar
|
226
|
+
end
|
227
|
+
|
228
|
+
self.all_ros = all_ordered_ros
|
229
|
+
all_ros.each.with_index { |ro, i| ro.index = i }
|
230
|
+
task.force_finish # Just in case
|
231
|
+
end
|
232
|
+
|
233
|
+
def build_iv_sub_ros_for(ro)
|
234
|
+
return if ro.iv_sub_ros.any?
|
235
|
+
|
236
|
+
iv_names = ro.value.instance_variables
|
237
|
+
return if iv_names.empty?
|
238
|
+
|
239
|
+
prog_task = Prog::Task.new(max_count: iv_names.count)
|
240
|
+
progress_bar << prog_task
|
241
|
+
|
242
|
+
iv_names.each do |iv_name|
|
243
|
+
iv_value = rescue_to_viewwrapped_error do
|
244
|
+
ro.value.instance_variable_get(iv_name)
|
245
|
+
end
|
246
|
+
iv_key = Scryglass::ViewWrapper.new(iv_name,
|
247
|
+
string: iv_name.to_s) # to_s removes ':'
|
248
|
+
ro.sub_ros << roify(iv_value,
|
249
|
+
parent_ro: ro,
|
250
|
+
key: iv_key,
|
251
|
+
key_value_relationship_indicator: ' : ',
|
252
|
+
special_sub_ro_type: :iv,
|
253
|
+
depth: ro.depth + 1)
|
254
|
+
prog_task.tick
|
255
|
+
print_progress_bar
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def build_ar_sub_ros_for(ro)
|
260
|
+
return if ro.ar_sub_ros.any?
|
261
|
+
return unless ro.value.class.respond_to?(:reflections)
|
262
|
+
|
263
|
+
reflections = ro.value.class.reflections
|
264
|
+
|
265
|
+
include_empty_associations =
|
266
|
+
Scryglass.config.include_empty_associations
|
267
|
+
include_through_associations =
|
268
|
+
Scryglass.config.include_through_associations
|
269
|
+
include_scoped_associations =
|
270
|
+
Scryglass.config.include_scoped_associations
|
271
|
+
show_association_types =
|
272
|
+
Scryglass.config.show_association_types
|
273
|
+
|
274
|
+
through_filter = lambda do |info|
|
275
|
+
include_through_associations || !info.options[:through]
|
276
|
+
end
|
277
|
+
|
278
|
+
scope_filter = lambda do |info|
|
279
|
+
include_scoped_associations || !info.scope
|
280
|
+
# This... `info.scope`
|
281
|
+
# is to get rid of extraneous custom scoped associations
|
282
|
+
# like current_primary_phone_number_record.
|
283
|
+
end
|
284
|
+
|
285
|
+
direct_close_reflections = reflections.select do |_, info|
|
286
|
+
through_filter.call(info) &&
|
287
|
+
scope_filter.call(info)
|
288
|
+
end
|
289
|
+
|
290
|
+
relation_names = direct_close_reflections.keys
|
291
|
+
return if relation_names.empty?
|
292
|
+
|
293
|
+
task = Prog::Task.new(max_count: relation_names.count)
|
294
|
+
progress_bar << task
|
295
|
+
|
296
|
+
direct_close_reflections.sort_by { |relation_name, _info| relation_name }
|
297
|
+
.each do |relation_name, info|
|
298
|
+
ar_value = Hexes.hide_db_outputs do
|
299
|
+
rescue_to_viewwrapped_error do
|
300
|
+
ro.value.send(relation_name)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
relationship_type = info.macro.to_s.split('_')
|
305
|
+
.map { |s| s[0].upcase }.join('')
|
306
|
+
relationship_type = "(#{relationship_type})"
|
307
|
+
|
308
|
+
is_through = info.options.keys.include?(:through)
|
309
|
+
if include_through_associations
|
310
|
+
through_indicator = is_through ? '(t)' : ' '
|
311
|
+
end
|
312
|
+
|
313
|
+
is_scoped = info.scope.present?
|
314
|
+
if include_scoped_associations
|
315
|
+
scoped_indicator = is_scoped ? '(s)' : ' '
|
316
|
+
end
|
317
|
+
|
318
|
+
relation_representation =
|
319
|
+
if show_association_types
|
320
|
+
"#{relationship_type}#{through_indicator}#{scoped_indicator} #{relation_name}"
|
321
|
+
else
|
322
|
+
relation_name.to_s
|
323
|
+
end
|
324
|
+
|
325
|
+
if ar_value.present? || include_empty_associations
|
326
|
+
ar_key = Scryglass::ViewWrapper.new(
|
327
|
+
relation_name,
|
328
|
+
string: relation_representation
|
329
|
+
)
|
330
|
+
ro.sub_ros << roify(ar_value,
|
331
|
+
parent_ro: ro,
|
332
|
+
key: ar_key,
|
333
|
+
key_value_relationship_indicator: ': ',
|
334
|
+
special_sub_ro_type: :ar,
|
335
|
+
depth: ro.depth + 1)
|
336
|
+
end
|
337
|
+
|
338
|
+
task.tick
|
339
|
+
print_progress_bar
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def build_enum_children_for(ro)
|
344
|
+
return if ro.enum_sub_ros.any?
|
345
|
+
return if ro.bucket?
|
346
|
+
return unless ro.value.is_a?(Enumerable)
|
347
|
+
|
348
|
+
pretend_klass = if ro.value.respond_to?(:keys)
|
349
|
+
Hash
|
350
|
+
elsif ro.value.respond_to?(:each)
|
351
|
+
Array
|
352
|
+
end
|
353
|
+
|
354
|
+
if pretend_klass == Hash
|
355
|
+
key_names = ro.value.keys
|
356
|
+
return if key_names.empty?
|
357
|
+
|
358
|
+
prog_task = Prog::Task.new(max_count: key_names.count)
|
359
|
+
progress_bar << prog_task
|
360
|
+
|
361
|
+
ro.value.each do |key, value|
|
362
|
+
ro.sub_ros << roify(value,
|
363
|
+
parent_ro: ro,
|
364
|
+
key: key,
|
365
|
+
key_value_relationship_indicator: ' => ',
|
366
|
+
special_sub_ro_type: :enum,
|
367
|
+
depth: ro.depth + 1)
|
368
|
+
prog_task.tick
|
369
|
+
print_progress_bar
|
370
|
+
end
|
371
|
+
elsif pretend_klass == Array
|
372
|
+
return if ro.value.count.zero?
|
373
|
+
|
374
|
+
prog_task = Prog::Task.new(max_count: ro.value.count)
|
375
|
+
progress_bar << prog_task
|
376
|
+
|
377
|
+
ro.value.each do |value|
|
378
|
+
ro.sub_ros << roify(value,
|
379
|
+
parent_ro: ro,
|
380
|
+
special_sub_ro_type: :enum,
|
381
|
+
depth: ro.depth + 1)
|
382
|
+
prog_task.tick
|
383
|
+
print_progress_bar
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
def rescue_to_viewwrapped_error
|
389
|
+
begin
|
390
|
+
successful_yielded_return = yield
|
391
|
+
rescue => e
|
392
|
+
legible_error_string = [e.message, *e.backtrace].join("\n")
|
393
|
+
viewwrapped_error = Scryglass::ViewWrapper.new(
|
394
|
+
legible_error_string,
|
395
|
+
string: '«ERROR»'
|
396
|
+
)
|
397
|
+
ensure
|
398
|
+
viewwrapped_error || successful_yielded_return
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
@@ -0,0 +1,514 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Scryglass::Session
|
4
|
+
include Scryglass::RoBuilder
|
5
|
+
|
6
|
+
using AnsilessStringRefinement
|
7
|
+
|
8
|
+
attr_accessor :all_ros, :current_ro, :special_command_targets
|
9
|
+
|
10
|
+
attr_accessor :current_view_coords, :current_lens, :current_subject_type,
|
11
|
+
:view_panels, :current_panel_type,
|
12
|
+
:progress_bar
|
13
|
+
|
14
|
+
attr_accessor :user_input, :last_search, :number_to_move
|
15
|
+
|
16
|
+
CURSOR_CHARACTER = '–' # These are en dashes (alt+dash), not hyphens or em dashes.
|
17
|
+
|
18
|
+
SEARCH_PROMPT = "\e[7mSearch for (regex, case-sensitive): /\e[00m"
|
19
|
+
|
20
|
+
SESSION_CLOSED_MESSAGE = '(Exited scry! Resume session with `scry` or `scry_resume`)'
|
21
|
+
|
22
|
+
SUBJECT_TYPES = [
|
23
|
+
:value,
|
24
|
+
:key
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
CSI = "\e[" # "(C)ontrol (S)equence (I)ntroducer" for ANSI sequences
|
28
|
+
|
29
|
+
def initialize(seed)
|
30
|
+
self.all_ros = []
|
31
|
+
self.current_lens = 0
|
32
|
+
self.current_subject_type = :value
|
33
|
+
self.current_panel_type = :tree
|
34
|
+
self.special_command_targets = []
|
35
|
+
self.number_to_move = ''
|
36
|
+
self.user_input = nil
|
37
|
+
self.progress_bar = Prog::Pipe.new
|
38
|
+
|
39
|
+
top_ro = roify(seed, parent_ro: nil, depth: 1)
|
40
|
+
top_ro.has_cursor = true
|
41
|
+
self.current_ro = top_ro
|
42
|
+
|
43
|
+
expand!(top_ro)
|
44
|
+
|
45
|
+
self.view_panels = {
|
46
|
+
tree: Scryglass::TreePanel.new(scry_session: self),
|
47
|
+
lens: Scryglass::LensPanel.new(scry_session: self),
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def run_scry_ui(actions:)
|
52
|
+
in_scry_session = true
|
53
|
+
redraw = true
|
54
|
+
|
55
|
+
case actions
|
56
|
+
when :record
|
57
|
+
$scry_session_actions_performed = []
|
58
|
+
when :playback
|
59
|
+
if $scry_session_actions_performed.blank?
|
60
|
+
raise 'Could not find recording of previous session\'s actions'
|
61
|
+
end
|
62
|
+
@input_stack = $scry_session_actions_performed.dup
|
63
|
+
end
|
64
|
+
|
65
|
+
# We print a full screen of lines so the first call of draw_screen doesn't
|
66
|
+
# write over any previous valuable content the user had in the console.
|
67
|
+
print Hexes.opacify_screen_string(Hexes.simple_screen_slice(boot_screen))
|
68
|
+
|
69
|
+
while in_scry_session
|
70
|
+
draw_screen if redraw
|
71
|
+
redraw = true
|
72
|
+
|
73
|
+
case actions
|
74
|
+
when :record
|
75
|
+
self.user_input = $stdin.getch
|
76
|
+
$scry_session_actions_performed << user_input
|
77
|
+
when :playback
|
78
|
+
if @input_stack.any? # (IV to be easily accessible for debugging)
|
79
|
+
self.user_input = @input_stack.shift
|
80
|
+
sleep 0.05
|
81
|
+
else
|
82
|
+
self.user_input = $stdin.getch
|
83
|
+
end
|
84
|
+
else
|
85
|
+
self.user_input = $stdin.getch
|
86
|
+
end
|
87
|
+
|
88
|
+
wait_start_time = Time.now
|
89
|
+
|
90
|
+
case user_input
|
91
|
+
when "\u0003"
|
92
|
+
set_console_cursor_below_content
|
93
|
+
raise IRB::Abort, 'Ctrl+C Detected'
|
94
|
+
when 'q'
|
95
|
+
in_scry_session = false
|
96
|
+
visually_close_ui
|
97
|
+
when '1'
|
98
|
+
self.number_to_move += '1'
|
99
|
+
redraw = false # This allows you to type multi-digit number very
|
100
|
+
# quickly and still have it process all the digits.
|
101
|
+
when '2'
|
102
|
+
self.number_to_move += '2'
|
103
|
+
redraw = false
|
104
|
+
when '3'
|
105
|
+
self.number_to_move += '3'
|
106
|
+
redraw = false
|
107
|
+
when '4'
|
108
|
+
self.number_to_move += '4'
|
109
|
+
redraw = false
|
110
|
+
when '5'
|
111
|
+
self.number_to_move += '5'
|
112
|
+
redraw = false
|
113
|
+
when '6'
|
114
|
+
self.number_to_move += '6'
|
115
|
+
redraw = false
|
116
|
+
when '7'
|
117
|
+
self.number_to_move += '7'
|
118
|
+
redraw = false
|
119
|
+
when '8'
|
120
|
+
self.number_to_move += '8'
|
121
|
+
redraw = false
|
122
|
+
when '9'
|
123
|
+
self.number_to_move += '9'
|
124
|
+
redraw = false
|
125
|
+
when '0'
|
126
|
+
if number_to_move.present? # You can append zeros to number_to_move...
|
127
|
+
self.number_to_move += '0'
|
128
|
+
redraw = false
|
129
|
+
else # ...but otherwise it's understood to be a view||cursor reset.
|
130
|
+
reset_the_view_or_cursor
|
131
|
+
end
|
132
|
+
when 'A' # Up arrow
|
133
|
+
action_count = number_to_move.present? ? number_to_move.to_i : 1
|
134
|
+
navigate_up_multiple(action_count)
|
135
|
+
|
136
|
+
self.number_to_move = ''
|
137
|
+
lens_view.recalculate_boundaries if current_panel_type == :lens
|
138
|
+
tree_view.slide_view_to_cursor
|
139
|
+
when 'B' # Down arrow
|
140
|
+
action_count = number_to_move.present? ? number_to_move.to_i : 1
|
141
|
+
navigate_down_multiple(action_count)
|
142
|
+
|
143
|
+
self.number_to_move = ''
|
144
|
+
lens_view.recalculate_boundaries if current_panel_type == :lens
|
145
|
+
tree_view.slide_view_to_cursor
|
146
|
+
when 'C' # Right arrow
|
147
|
+
expand_targets
|
148
|
+
when 'D' # Left arrow
|
149
|
+
collapse_targets
|
150
|
+
lens_view.recalculate_boundaries if current_panel_type == :lens
|
151
|
+
when ' '
|
152
|
+
toggle_view_panel
|
153
|
+
lens_view.recalculate_boundaries if current_panel_type == :lens
|
154
|
+
when 'l'
|
155
|
+
scroll_lens_type
|
156
|
+
lens_view.recalculate_boundaries if current_panel_type == :lens
|
157
|
+
when 'L'
|
158
|
+
toggle_current_subject_type
|
159
|
+
lens_view.recalculate_boundaries if current_panel_type == :lens
|
160
|
+
when 'w'
|
161
|
+
current_view_panel.move_view_up(5)
|
162
|
+
when 's'
|
163
|
+
current_view_panel.move_view_down(5)
|
164
|
+
when 'a'
|
165
|
+
current_view_panel.move_view_left(5)
|
166
|
+
when 'd'
|
167
|
+
current_view_panel.move_view_right(5)
|
168
|
+
when '∑' # Alt+w
|
169
|
+
current_view_panel.move_view_up(50)
|
170
|
+
when 'ß' # Alt+s
|
171
|
+
current_view_panel.move_view_down(50)
|
172
|
+
when 'å' # Alt+a
|
173
|
+
current_view_panel.move_view_left(50)
|
174
|
+
when '∂' # Alt+d
|
175
|
+
current_view_panel.move_view_right(50)
|
176
|
+
when '?'
|
177
|
+
in_scry_session = run_help_screen_ui
|
178
|
+
when '@'
|
179
|
+
build_instance_variables_for_target_ros
|
180
|
+
tree_view.recalculate_boundaries
|
181
|
+
tree_view.slide_view_to_cursor # Just a nice-to-have
|
182
|
+
when '.'
|
183
|
+
build_activerecord_relations_for_target_ros
|
184
|
+
tree_view.recalculate_boundaries
|
185
|
+
tree_view.slide_view_to_cursor # Just a nice-to-have
|
186
|
+
when '('
|
187
|
+
build_enum_children_for_target_ros
|
188
|
+
tree_view.recalculate_boundaries
|
189
|
+
tree_view.slide_view_to_cursor # Just a nice-to-have
|
190
|
+
when '|'
|
191
|
+
sibling_ros = if current_ro.top_ro?
|
192
|
+
[top_ro]
|
193
|
+
else
|
194
|
+
current_ro.parent_ro.sub_ros.dup # If we don't dup,
|
195
|
+
# then '-' can remove ros from `sub_ros`.
|
196
|
+
end
|
197
|
+
if special_command_targets.sort == sibling_ros.sort
|
198
|
+
self.special_command_targets = []
|
199
|
+
else
|
200
|
+
self.special_command_targets = sibling_ros
|
201
|
+
end
|
202
|
+
when '*'
|
203
|
+
all_the_ros = all_ros.dup # If we don't dup,
|
204
|
+
# then '-' can remove ros from all_ros.
|
205
|
+
if special_command_targets.sort == all_the_ros.sort
|
206
|
+
self.special_command_targets = []
|
207
|
+
else
|
208
|
+
self.special_command_targets = all_the_ros
|
209
|
+
end
|
210
|
+
when '-'
|
211
|
+
if special_command_targets.include?(current_ro)
|
212
|
+
special_command_targets.delete(current_ro)
|
213
|
+
else
|
214
|
+
special_command_targets << current_ro
|
215
|
+
end
|
216
|
+
when '/'
|
217
|
+
$stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
|
218
|
+
$stdout.write SEARCH_PROMPT
|
219
|
+
$stdout.write "#{CSI}1;#{SEARCH_PROMPT.ansiless_length + 1}H" # (Moves
|
220
|
+
# console cursor to just after the search prompt, before user types)
|
221
|
+
query = $stdin.gets.chomp
|
222
|
+
if query.present?
|
223
|
+
self.last_search = query
|
224
|
+
go_to_next_search_result
|
225
|
+
end
|
226
|
+
when 'n'
|
227
|
+
if last_search
|
228
|
+
go_to_next_search_result
|
229
|
+
else
|
230
|
+
$stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
|
231
|
+
$stdout.write "\e[7m-- No Search has been entered --\e[00m"
|
232
|
+
sleep 2
|
233
|
+
end
|
234
|
+
when "\r" # [ENTER]
|
235
|
+
visually_close_ui
|
236
|
+
return subjects_of_target_ros
|
237
|
+
end
|
238
|
+
|
239
|
+
print "\a" if Time.now - wait_start_time > 4 && user_input != '?' # (Audio 'beep')
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def top_ro
|
244
|
+
all_ros.first
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
def print_progress_bar
|
250
|
+
screen_height, _screen_width = $stdout.winsize
|
251
|
+
bar = progress_bar.to_s
|
252
|
+
$stdout.write "#{CSI}#{screen_height};1H" # (Moves console cursor to bottom left corner)
|
253
|
+
print bar if bar.present?
|
254
|
+
end
|
255
|
+
|
256
|
+
def current_view_panel
|
257
|
+
view_panels[current_panel_type]
|
258
|
+
end
|
259
|
+
|
260
|
+
def tree_view
|
261
|
+
view_panels[:tree]
|
262
|
+
end
|
263
|
+
|
264
|
+
def lens_view
|
265
|
+
view_panels[:lens]
|
266
|
+
end
|
267
|
+
|
268
|
+
def colorize(screen_string)
|
269
|
+
dot = '•'
|
270
|
+
cyan_dot = "\e[36m#{dot}\e[00m" # cyan then back to *default*
|
271
|
+
screen_string.gsub!('•', cyan_dot)
|
272
|
+
|
273
|
+
screen_string
|
274
|
+
end
|
275
|
+
|
276
|
+
def display_active_searching_indicator
|
277
|
+
$stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
|
278
|
+
message = ' Searching... '
|
279
|
+
pad = SEARCH_PROMPT.length - message.length
|
280
|
+
wing = '-' * (pad / 2)
|
281
|
+
|
282
|
+
$stdout.write "\e[7m#{wing + message + wing}\e[00m"
|
283
|
+
end
|
284
|
+
|
285
|
+
def go_to_next_search_result
|
286
|
+
display_active_searching_indicator
|
287
|
+
|
288
|
+
cut_point = current_ro.index
|
289
|
+
search_set = ((cut_point + 1)...all_ros.count).to_a + (0...cut_point).to_a
|
290
|
+
|
291
|
+
task = Prog::Task.new(max_count: search_set.count)
|
292
|
+
progress_bar << task
|
293
|
+
|
294
|
+
index_of_next_match = search_set.find do |index|
|
295
|
+
scanned_ro = all_ros[index]
|
296
|
+
task.tick
|
297
|
+
print_progress_bar
|
298
|
+
scanned_ro.key_string =~ /#{last_search}/ ||
|
299
|
+
(scanned_ro.nugget? && scanned_ro.value_string =~ /#{last_search}/)
|
300
|
+
end
|
301
|
+
task.force_finish
|
302
|
+
|
303
|
+
if index_of_next_match
|
304
|
+
next_found_ro = all_ros[index_of_next_match]
|
305
|
+
move_cursor_to(next_found_ro)
|
306
|
+
|
307
|
+
scanning_ro = next_found_ro
|
308
|
+
while scanning_ro.parent_ro
|
309
|
+
expand!(scanning_ro.parent_ro)
|
310
|
+
scanning_ro = scanning_ro.parent_ro
|
311
|
+
end
|
312
|
+
|
313
|
+
tree_view.recalculate_boundaries # Yes, necessary :)
|
314
|
+
lens_view.recalculate_boundaries # Yes, necessary :)
|
315
|
+
tree_view.current_view_coords = { y: 0, x: 0 }
|
316
|
+
tree_view.slide_view_to_cursor
|
317
|
+
else
|
318
|
+
$stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
|
319
|
+
message = ' No Match Found '
|
320
|
+
pad = SEARCH_PROMPT.length - message.length
|
321
|
+
wing = '-' * (pad / 2)
|
322
|
+
|
323
|
+
$stdout.write "\e[7m#{wing + message + wing}\e[00m"
|
324
|
+
sleep 2
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def run_help_screen_ui
|
329
|
+
screen_height, _screen_width = $stdout.winsize
|
330
|
+
|
331
|
+
in_help_screen = true
|
332
|
+
current_help_screen_index = 0
|
333
|
+
help_screens = [Scryglass::HELP_SCREEN, Scryglass::HELP_SCREEN_ADVANCED]
|
334
|
+
|
335
|
+
while in_help_screen
|
336
|
+
current_help_screen = help_screens[current_help_screen_index]
|
337
|
+
sliced_help_screen = Hexes.simple_screen_slice(current_help_screen)
|
338
|
+
help_screen_string = Hexes.opacify_screen_string(sliced_help_screen)
|
339
|
+
Hexes.overwrite_screen(help_screen_string)
|
340
|
+
help_screen_user_input = $stdin.getch
|
341
|
+
|
342
|
+
case help_screen_user_input
|
343
|
+
when '?'
|
344
|
+
current_help_screen_index += 1
|
345
|
+
when 'q'
|
346
|
+
$stdout.write "#{CSI}#{screen_height};1H" # (Moves console cursor to
|
347
|
+
# bottom left corner). This helps 'q' not print the console prompt at
|
348
|
+
# the top of the screen, overlapping with the old display.
|
349
|
+
return false
|
350
|
+
when "\u0003"
|
351
|
+
screen_height, _screen_width = $stdout.winsize
|
352
|
+
puts "\n" * screen_height
|
353
|
+
raise IRB::Abort, 'Ctrl+C Detected'
|
354
|
+
end
|
355
|
+
|
356
|
+
current_help_screen = help_screens[current_help_screen_index]
|
357
|
+
unless current_help_screen
|
358
|
+
return true
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def collapse_targets
|
364
|
+
if special_command_targets.any?
|
365
|
+
target_ros = special_command_targets.dup # dup because some commands
|
366
|
+
# create ros which are added to all_ros and then this process starts
|
367
|
+
# adding them to the list of things it tries to act on!
|
368
|
+
target_ros.each { |target_ro| collapse!(target_ro) }
|
369
|
+
self.special_command_targets = []
|
370
|
+
elsif current_ro.expanded
|
371
|
+
collapse!(current_ro)
|
372
|
+
elsif current_ro.parent_ro
|
373
|
+
collapse!(current_ro.parent_ro)
|
374
|
+
end
|
375
|
+
|
376
|
+
move_cursor_to(current_ro.parent_ro) until current_ro.visible?
|
377
|
+
tree_view.slide_view_to_cursor
|
378
|
+
tree_view.recalculate_boundaries # TODO: should these be conditional? If they are, I might need a potential tree view recalc after toggling lens view to tree view.
|
379
|
+
end
|
380
|
+
|
381
|
+
def expand_targets
|
382
|
+
if special_command_targets.any?
|
383
|
+
target_ros = special_command_targets.dup # dup because some commands
|
384
|
+
# create ros which are added to all_ros and then this process starts
|
385
|
+
# adding them to the list of things it tries to act on!
|
386
|
+
target_ros.each { |target_ro| expand!(target_ro) }
|
387
|
+
self.special_command_targets = []
|
388
|
+
else
|
389
|
+
expand!(current_ro)
|
390
|
+
end
|
391
|
+
tree_view.recalculate_boundaries
|
392
|
+
end
|
393
|
+
|
394
|
+
def reset_the_view_or_cursor
|
395
|
+
if current_view_panel.current_view_coords != { x: 0, y: 0 }
|
396
|
+
current_view_panel.current_view_coords = { x: 0, y: 0 }
|
397
|
+
elsif current_panel_type == :tree
|
398
|
+
move_cursor_to(top_ro)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
def draw_screen
|
403
|
+
current_view_panel.ensure_correct_view_coords
|
404
|
+
screen_string = current_view_panel.screen_string
|
405
|
+
|
406
|
+
screen_string = colorize(screen_string) if Scryglass.config.dot_coloring
|
407
|
+
Hexes.overwrite_screen(screen_string)
|
408
|
+
$stdout.write "#{CSI}1;1H" # Moves terminal cursor to top left corner,
|
409
|
+
# mostly for consistency.
|
410
|
+
end
|
411
|
+
|
412
|
+
def set_console_cursor_below_content
|
413
|
+
bare_screen_string =
|
414
|
+
current_view_panel.visible_header_string + "\n" +
|
415
|
+
current_view_panel.visible_body_string
|
416
|
+
split_lines = bare_screen_string.split("\n")
|
417
|
+
rows_filled = split_lines.count
|
418
|
+
$stdout.write "#{CSI}#{rows_filled};1H\n" # Moves console cursor to bottom
|
419
|
+
# of *content*, then one more.
|
420
|
+
end
|
421
|
+
|
422
|
+
def visually_close_ui
|
423
|
+
_screen_height, screen_width = $stdout.winsize
|
424
|
+
set_console_cursor_below_content
|
425
|
+
puts '·' * screen_width, "\n"
|
426
|
+
puts SESSION_CLOSED_MESSAGE
|
427
|
+
end
|
428
|
+
|
429
|
+
def subjects_of_target_ros
|
430
|
+
if special_command_targets.any?
|
431
|
+
return_targets = special_command_targets
|
432
|
+
self.special_command_targets = []
|
433
|
+
return return_targets.map(&:current_subject)
|
434
|
+
end
|
435
|
+
|
436
|
+
current_ro.current_subject
|
437
|
+
end
|
438
|
+
|
439
|
+
def navigate_up_multiple(action_count)
|
440
|
+
task = Prog::Task.new(max_count: action_count)
|
441
|
+
progress_bar << task
|
442
|
+
action_count.times do
|
443
|
+
navigate_up
|
444
|
+
task.tick
|
445
|
+
print_progress_bar
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
def navigate_down_multiple(action_count)
|
450
|
+
task = Prog::Task.new(max_count: action_count)
|
451
|
+
progress_bar << task
|
452
|
+
action_count.times do
|
453
|
+
navigate_down
|
454
|
+
task.tick
|
455
|
+
print_progress_bar
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def expand!(ro)
|
460
|
+
ro.expanded = true if ro.sub_ros.any?
|
461
|
+
end
|
462
|
+
|
463
|
+
def collapse!(ro)
|
464
|
+
ro.expanded = false if ro.expanded
|
465
|
+
end
|
466
|
+
|
467
|
+
def toggle_view_panel
|
468
|
+
self.current_panel_type =
|
469
|
+
case current_panel_type
|
470
|
+
when :tree
|
471
|
+
:lens
|
472
|
+
when :lens
|
473
|
+
:tree
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
def toggle_current_subject_type
|
478
|
+
self.current_subject_type =
|
479
|
+
case current_subject_type
|
480
|
+
when :value
|
481
|
+
:key
|
482
|
+
when :key
|
483
|
+
:value
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def scroll_lens_type
|
488
|
+
self.current_lens += 1
|
489
|
+
end
|
490
|
+
|
491
|
+
def move_cursor_to(new_ro)
|
492
|
+
current_ro.has_cursor = false
|
493
|
+
new_ro.has_cursor = true
|
494
|
+
self.current_ro = new_ro
|
495
|
+
end
|
496
|
+
|
497
|
+
def navigate_up
|
498
|
+
next_up = current_ro.next_visible_ro_up
|
499
|
+
move_cursor_to(next_up) if next_up
|
500
|
+
end
|
501
|
+
|
502
|
+
def navigate_down
|
503
|
+
next_down = current_ro.next_visible_ro_down
|
504
|
+
move_cursor_to(next_down) if next_down
|
505
|
+
end
|
506
|
+
|
507
|
+
def boot_screen
|
508
|
+
screen_height, screen_width = $stdout.winsize
|
509
|
+
stars = (1..(screen_height * screen_width))
|
510
|
+
.to_a
|
511
|
+
.map { rand(100).zero? ? '.' : ' ' }
|
512
|
+
stars.each_slice(screen_width).map { |set| set.join('') }.join("\n")
|
513
|
+
end
|
514
|
+
end
|