scryglass 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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