scryglass 0.1.0

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.
@@ -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