scryglass 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -43,14 +43,22 @@ module Scryglass
43
43
  self.key_string = key.to_s.clip_at(key_clip_length)
44
44
  self.key = key.model
45
45
  else
46
- self.key_string = key.inspect.clip_at(key_clip_length)
46
+ # Note: `.inspect` may return *true newlines* for objects with a custom
47
+ # `.inspect`, which will sabotage scry's display, so we gsub thusly:
48
+ self.key_string = key.inspect
49
+ .gsub("\n", "\\n")
50
+ .clip_at(key_clip_length)
47
51
  self.key = key
48
52
  end
49
53
  if val.class == Scryglass::ViewWrapper
50
54
  self.value_string = val.to_s.clip_at(value_clip_length)
51
55
  self.value = val.model
52
56
  else
53
- self.value_string = val.inspect.clip_at(value_clip_length)
57
+ # Note: `.inspect` may return *true newlines* for objects with a custom
58
+ # `.inspect`, which will sabotage scry's display, so we gsub thusly:
59
+ self.value_string = val.inspect
60
+ .gsub("\n", "\\n")
61
+ .clip_at(value_clip_length)
54
62
  self.value = val
55
63
  end
56
64
 
@@ -78,9 +86,10 @@ module Scryglass
78
86
 
79
87
  key_value_spacer =
80
88
  key_value_pair? ? key_string + key_value_relationship_indicator : ''
81
-
89
+ dot = '•'
90
+ dot = "\e[36m#{dot}\e[00m" if Scryglass.config.dot_coloring # cyan then back to *default*
82
91
  special_sub_ro_expansion_indicator =
83
- special_sub_ros.any? && !expanded ? '•' : ' '
92
+ special_sub_ros.any? && !expanded ? dot : ' '
84
93
 
85
94
  left_fill_string + special_sub_ro_expansion_indicator +
86
95
  key_value_spacer + value_indicator
@@ -168,12 +177,12 @@ module Scryglass
168
177
  !!key_value_relationship_indicator
169
178
  end
170
179
 
171
- private
172
-
173
180
  def special_sub_ros
174
181
  sub_ros.select(&:special_sub_ro_type)
175
182
  end
176
183
 
184
+ private
185
+
177
186
  def normal_sub_ros
178
187
  sub_ros.reject(&:special_sub_ro_type)
179
188
  end
@@ -187,7 +196,9 @@ module Scryglass
187
196
  # communicating with a solid preexisting symbol), but keeping the idea here:
188
197
  # sub_ros_order_of_magnitude = normal_sub_ros.count.to_s.length
189
198
  # wrappers.dup.insert(1, '•' * sub_ros_order_of_magnitude)
190
- wrappers.dup.insert(1, '•••')
199
+ dots = '•••'
200
+ dots = "\e[36m#{dots}\e[00m" if Scryglass.config.dot_coloring # cyan then back to *default*
201
+ wrappers.dup.insert(1, dots)
191
202
  else
192
203
  wrappers
193
204
  end
@@ -214,20 +225,78 @@ module Scryglass
214
225
  (tab_length * depth) + consistent_margin
215
226
  end
216
227
 
228
+ def cursor_char
229
+ Scryglass::Session::CURSOR_CHARACTER
230
+ end
231
+
217
232
  def cursor_string
218
- cursor = Scryglass::Session::CURSOR_CHARACTER * cursor_length
233
+ cursor = cursor_char * cursor_length
234
+
235
+ cursor[0] = enum_status_char
236
+ cursor[1] = iv_status_char
237
+ cursor[2] = ar_status_char
238
+
239
+ cursor
240
+ end
241
+
242
+ def enum_status_char
243
+ enum_worth_checking = nugget? && value.is_a?(Enumerable)
244
+ return cursor_char unless enum_worth_checking
245
+
246
+ enum_check = Scryglass::Ro.safe_quick_check do
247
+ # value.any? Can take an eternity for a few specific objects, breaking
248
+ # the session when the cursor passes over them. Also breaks on read-
249
+ # locked IO objects.
250
+ enum_sub_ros.empty? && value.any?
251
+ end
252
+
253
+ return 'X' if enum_check.nil?
219
254
 
220
- if nugget? && has_cursor && value.is_a?(Enumerable) &&
221
- value.any? &&
222
- enum_sub_ros.empty?
223
- cursor[0] = '('
255
+ return '(' if enum_check
256
+
257
+ cursor_char
258
+ end
259
+
260
+ def iv_status_char
261
+ return cursor_char unless iv_sub_ros.empty?
262
+
263
+ iv_check = Scryglass::Ro.safe_quick_check do
264
+ value.instance_variables.any?
224
265
  end
225
266
 
226
- if value.instance_variables.any? && iv_sub_ros.empty?
227
- cursor[1] = '@'
267
+ return 'X' if iv_check.nil?
268
+
269
+ return '@' if iv_check
270
+
271
+ cursor_char
272
+ end
273
+
274
+ def ar_status_char
275
+ return cursor_char unless ar_sub_ros.empty?
276
+
277
+ iv_check = Scryglass::Ro.safe_quick_check do
278
+ # Currently, this will always indicate hidden secrets if the object, with
279
+ # the given Scryglass config, doesn't yield any ar_sub_ros upon trying '.'
280
+ value.class.respond_to?(:reflections) # TODO: maybe dig more here?
228
281
  end
229
282
 
230
- cursor
283
+ return 'X' if iv_check.nil?
284
+
285
+ return '·' if iv_check
286
+
287
+ cursor_char
288
+ end
289
+
290
+ class << self
291
+ def safe_quick_check
292
+ begin
293
+ Timeout.timeout(0.05) do
294
+ yield
295
+ end
296
+ rescue
297
+ nil
298
+ end
299
+ end
231
300
  end
232
301
  end
233
302
  end
@@ -135,8 +135,49 @@ module Scryglass
135
135
  new_ro
136
136
  end
137
137
 
138
+ def add_message_no_items_found
139
+ message = { text: "No new sub-items were found with '#{last_keypress}'",
140
+ end_time: Time.now + 1.5 }
141
+ self.current_warning_messages << message
142
+ end
143
+
144
+ def smart_open_target_ros
145
+ original_ro_count = all_ros.count
146
+ original_special_sub_ro_count = current_ro.special_sub_ros.count
147
+
148
+ if special_command_targets.any?
149
+ task = Prog::Task.new(max_count: special_command_targets.count)
150
+ progress_bar << task
151
+
152
+ target_ros = special_command_targets.dup # dup because some commands
153
+ # create ros which are added to all_ros and then this process starts
154
+ # adding them to the list of things it tries to act on!
155
+ target_ros.each do |target_ro|
156
+ smart_open(target_ro)
157
+ task.tick
158
+ print_progress_bar
159
+ end
160
+ self.special_command_targets = []
161
+ else
162
+ smart_open(current_ro)
163
+
164
+ new_special_sub_ro_count = current_ro.special_sub_ros.count
165
+ new_sub_ros_were_added = new_special_sub_ro_count != original_special_sub_ro_count
166
+
167
+ if new_sub_ros_were_added
168
+ expand!(current_ro)
169
+ else
170
+ add_message_no_items_found
171
+ end
172
+ end
173
+
174
+ new_ro_count = all_ros.count
175
+ recalculate_indeces unless new_ro_count == original_ro_count
176
+ end
177
+
138
178
  def build_instance_variables_for_target_ros
139
- original_ro_total = all_ros.count
179
+ original_ro_count = all_ros.count
180
+ original_iv_sub_ro_count = current_ro.iv_sub_ros.count
140
181
 
141
182
  if special_command_targets.any?
142
183
  task = Prog::Task.new(max_count: special_command_targets.count)
@@ -153,15 +194,24 @@ module Scryglass
153
194
  self.special_command_targets = []
154
195
  else
155
196
  build_iv_sub_ros_for(current_ro)
156
- expand!(current_ro) if current_ro.iv_sub_ros.any?
197
+
198
+ new_iv_sub_ro_count = current_ro.iv_sub_ros.count
199
+ new_iv_sub_ros_were_added = new_iv_sub_ro_count != original_iv_sub_ro_count
200
+
201
+ if new_iv_sub_ros_were_added
202
+ expand!(current_ro)
203
+ else
204
+ add_message_no_items_found
205
+ end
157
206
  end
158
207
 
159
- new_ro_total = all_ros.count
160
- recalculate_indeces unless new_ro_total == original_ro_total
208
+ new_ro_count = all_ros.count
209
+ recalculate_indeces unless new_ro_count == original_ro_count
161
210
  end
162
211
 
163
212
  def build_activerecord_relations_for_target_ros
164
- original_ro_total = all_ros.count
213
+ original_ro_count = all_ros.count
214
+ original_ar_sub_ro_count = current_ro.ar_sub_ros.count
165
215
 
166
216
  if special_command_targets.any?
167
217
  task = Prog::Task.new(max_count: special_command_targets.count)
@@ -178,15 +228,24 @@ module Scryglass
178
228
  self.special_command_targets = []
179
229
  else
180
230
  build_ar_sub_ros_for(current_ro)
181
- expand!(current_ro) if current_ro.ar_sub_ros.any?
231
+
232
+ new_ar_sub_ro_count = current_ro.ar_sub_ros.count
233
+ new_ar_sub_ros_were_added = new_ar_sub_ro_count != original_ar_sub_ro_count
234
+
235
+ if new_ar_sub_ros_were_added
236
+ expand!(current_ro)
237
+ else
238
+ add_message_no_items_found
239
+ end
182
240
  end
183
- new_ro_total = all_ros.count
241
+ new_ro_count = all_ros.count
184
242
 
185
- recalculate_indeces unless new_ro_total == original_ro_total
243
+ recalculate_indeces unless new_ro_count == original_ro_count
186
244
  end
187
245
 
188
246
  def build_enum_children_for_target_ros
189
- original_ro_total = all_ros.count
247
+ original_ro_count = all_ros.count
248
+ original_enum_sub_ro_count = current_ro.enum_sub_ros.count
190
249
 
191
250
  if special_command_targets.any?
192
251
  task = Prog::Task.new(max_count: special_command_targets.count)
@@ -196,18 +255,32 @@ module Scryglass
196
255
  # create ros which are added to all_ros and then this process starts
197
256
  # adding them to the list of things it tries to act on!
198
257
  target_ros.each do |target_ro|
199
- build_enum_children_for(target_ro)
258
+ build_enum_sub_ros_for(target_ro)
200
259
  task.tick
201
260
  print_progress_bar
202
261
  end
203
262
  self.special_command_targets = []
204
263
  else
205
- build_enum_children_for(current_ro)
206
- expand!(current_ro) if current_ro.enum_sub_ros.any?
264
+ build_enum_sub_ros_for(current_ro)
265
+
266
+ new_enum_sub_ro_count = current_ro.enum_sub_ros.count
267
+ new_enum_sub_ros_were_added = new_enum_sub_ro_count != original_enum_sub_ro_count
268
+
269
+ if new_enum_sub_ros_were_added
270
+ expand!(current_ro)
271
+ else
272
+ add_message_no_items_found
273
+ end
207
274
  end
208
275
 
209
- new_ro_total = all_ros.count
210
- recalculate_indeces unless new_ro_total == original_ro_total
276
+ new_ro_count = all_ros.count
277
+ recalculate_indeces unless new_ro_count == original_ro_count
278
+ end
279
+
280
+ def smart_open(ro)
281
+ build_ar_sub_ros_for(ro) ||
282
+ build_iv_sub_ros_for(ro) ||
283
+ build_enum_sub_ros_for(ro)
211
284
  end
212
285
 
213
286
  def recalculate_indeces
@@ -254,6 +327,8 @@ module Scryglass
254
327
  prog_task.tick
255
328
  print_progress_bar
256
329
  end
330
+
331
+ true
257
332
  end
258
333
 
259
334
  def build_ar_sub_ros_for(ro)
@@ -322,7 +397,7 @@ module Scryglass
322
397
  relation_name.to_s
323
398
  end
324
399
 
325
- if (!ar_value || ar_value.empty?) || include_empty_associations
400
+ if (!ar_value || (ar_value.respond_to?(:empty?) && ar_value.empty?)) || include_empty_associations
326
401
  ar_key = Scryglass::ViewWrapper.new(
327
402
  relation_name,
328
403
  string: relation_representation
@@ -338,9 +413,11 @@ module Scryglass
338
413
  task.tick
339
414
  print_progress_bar
340
415
  end
416
+
417
+ true if ro.ar_sub_ros.any?
341
418
  end
342
419
 
343
- def build_enum_children_for(ro)
420
+ def build_enum_sub_ros_for(ro)
344
421
  return if ro.enum_sub_ros.any?
345
422
  return if ro.bucket?
346
423
  return unless ro.value.is_a?(Enumerable)
@@ -383,6 +460,8 @@ module Scryglass
383
460
  print_progress_bar
384
461
  end
385
462
  end
463
+
464
+ true
386
465
  end
387
466
 
388
467
  def rescue_to_viewwrapped_error
@@ -9,15 +9,18 @@ class Scryglass::Session
9
9
 
10
10
  attr_accessor :current_view_coords, :current_lens, :current_subject_type,
11
11
  :view_panels, :current_panel_type,
12
- :progress_bar
12
+ :progress_bar, :current_warning_messages
13
13
 
14
14
  attr_accessor :user_signals, :last_search, :number_to_move
15
15
 
16
+ attr_accessor :session_manager, :signal_to_manager, :session_is_current,
17
+ :tab_icon, :session_view_start_time
18
+
16
19
  CURSOR_CHARACTER = '–' # These are en dashes (alt+dash), not hyphens or em dashes.
17
20
 
18
- SEARCH_PROMPT = "\e[7mSearch for (regex, case-sensitive): /\e[00m"
21
+ SEARCH_PROMPT = "\e[7mSearch for (regex, case-sensitive): /\e[00m"
19
22
 
20
- SESSION_CLOSED_MESSAGE = '(Exited scry! Resume session with `scry` or `scry_resume`)'
23
+ VARNAME_PROMPT = "\e[7mName your object(s): @\e[00m"
21
24
 
22
25
  SUBJECT_TYPES = [
23
26
  :value,
@@ -26,6 +29,66 @@ class Scryglass::Session
26
29
 
27
30
  CSI = "\e[" # "(C)ontrol (S)equence (I)ntroducer" for ANSI sequences
28
31
 
32
+ KEY_MAP = {
33
+ escape: 'esc', # Not a normal keystroke, see: genuine_escape_key_press
34
+ ctrl_c: "\u0003",
35
+ quit_session: 'q',
36
+ delete_session_tab: 'Q',
37
+ change_session_right: "\t", # Tab
38
+ change_session_left: 'Z', # Shift+Tab (well, one of its signals, after "\e" and "[")
39
+ digit_1: '1',
40
+ digit_2: '2',
41
+ digit_3: '3',
42
+ digit_4: '4',
43
+ digit_5: '5',
44
+ digit_6: '6',
45
+ digit_7: '7',
46
+ digit_8: '8',
47
+ digit_9: '9',
48
+ digit_0: '0',
49
+ move_cursor_up: 'A', # Up arrow (well, one of its signals, after "\e" and "[")
50
+ move_cursor_down: 'B', # Down arrow (well, one of its signals, after "\e" and "[")
51
+ open_bucket: 'C', # Right arrow (well, one of its signals, after "\e" and "[")
52
+ close_bucket: 'D', # Left arrow (well, one of its signals, after "\e" and "[")
53
+ homerow_move_cursor_up: 'k', # To be like VIM arrow keys
54
+ homerow_move_cursor_up_fast: 'K', # To be like VIM arrow keys
55
+ homerow_move_cursor_down: 'j', # To be like VIM arrow keys
56
+ homerow_move_cursor_down_fast: 'J', # To be like VIM arrow keys
57
+ homerow_open_bucket: 'l', # To be like VIM arrow keys
58
+ homerow_close_bucket: 'h', # To be like VIM arrow keys
59
+ # Note, shift-UP and shift-DOWN are not here, as those work very
60
+ # differently: by virtue of the type-a-number-first functionality.
61
+ toggle_view_panel: ' ',
62
+ switch_lens: '>',
63
+ switch_subject_type: '<',
64
+ move_view_up: 'w',
65
+ move_view_down: 's',
66
+ move_view_left: 'a',
67
+ move_view_right: 'd',
68
+ move_view_up_fast: '∑', # Alt+w
69
+ move_view_down_fast: 'ß', # Alt+s
70
+ move_view_left_fast: 'å', # Alt+a
71
+ move_view_right_fast: '∂', # Alt+d
72
+ control_screen: '?',
73
+ build_instance_variables: '@',
74
+ build_ar_relations: '.',
75
+ build_enum_children: '(',
76
+ smart_open: 'o',
77
+ select_siblings: '|',
78
+ select_all: '*',
79
+ select_current: '-',
80
+ start_search: '/',
81
+ continue_search: 'n',
82
+ return_objects: "\r", # [ENTER],
83
+ name_objects: "="
84
+ }.freeze
85
+
86
+ PATIENT_ACTIONS = [
87
+ :control_screen,
88
+ :escape,
89
+ :name_objects,
90
+ ].freeze
91
+
29
92
  def initialize(seed)
30
93
  self.all_ros = []
31
94
  self.current_lens = 0
@@ -35,6 +98,12 @@ class Scryglass::Session
35
98
  self.number_to_move = ''
36
99
  self.user_signals = []
37
100
  self.progress_bar = Prog::Pipe.new
101
+ self.current_warning_messages = []
102
+ self.session_manager = nil
103
+ self.signal_to_manager = nil
104
+ self.tab_icon = nil
105
+ self.session_is_current = false
106
+ self.session_view_start_time = nil
38
107
 
39
108
  top_ro = roify(seed, parent_ro: nil, depth: 1)
40
109
  top_ro.has_cursor = true
@@ -48,9 +117,19 @@ class Scryglass::Session
48
117
  }
49
118
  end
50
119
 
51
- def run_scry_ui(actions:)
52
- in_scry_session = true
120
+ def top_ro
121
+ all_ros.first
122
+ end
123
+
124
+ def last_keypress
125
+ last_two_signals = user_signals.last(2)
126
+ last_two_signals.last || last_two_signals.first
127
+ end
128
+
129
+ def run_scry_ui
53
130
  redraw = true
131
+ signal_to_manager = nil
132
+ self.session_view_start_time = Time.now # For this particular tab/session
54
133
 
55
134
  ## On hold: Record/Playback Functionality:
56
135
  # case actions
@@ -67,7 +146,7 @@ class Scryglass::Session
67
146
  # write over any previous valuable content the user had in the console.
68
147
  print Hexes.opacify_screen_string(Hexes.simple_screen_slice(boot_screen))
69
148
 
70
- while in_scry_session
149
+ while true
71
150
  draw_screen if redraw
72
151
  redraw = true
73
152
 
@@ -93,108 +172,131 @@ class Scryglass::Session
93
172
 
94
173
  case new_signal
95
174
  when nil
96
- when 'esc'
175
+ when KEY_MAP[:escape]
97
176
  case current_panel_type
98
177
  when :lens
99
178
  self.current_panel_type = :tree
100
179
  when :tree
101
180
  clear_tracked_values
102
181
  end
103
- when "\u0003"
182
+ when KEY_MAP[:ctrl_c]
104
183
  set_console_cursor_below_content
105
184
  raise IRB::Abort, 'Ctrl+C Detected'
106
- when 'q'
107
- in_scry_session = false
108
- visually_close_ui
109
- when '1'
185
+ when KEY_MAP[:quit_session]
186
+ self.signal_to_manager = :quit
187
+ return
188
+ when KEY_MAP[:delete_session_tab]
189
+ self.signal_to_manager = :delete
190
+ return
191
+ when KEY_MAP[:control_screen]
192
+ remain_in_scry_session = run_help_screen_ui
193
+ unless remain_in_scry_session
194
+ self.signal_to_manager = :quit_from_help
195
+ return
196
+ end
197
+ when KEY_MAP[:digit_1]
110
198
  self.number_to_move += '1'
111
- redraw = false # This allows you to type multi-digit number very
112
- # quickly and still have it process all the digits.
113
- when '2'
199
+ # This allows you to type multi-digit number very
200
+ # quickly and still have it process all the digits:
201
+ redraw = false
202
+ when KEY_MAP[:digit_2]
114
203
  self.number_to_move += '2'
115
204
  redraw = false
116
- when '3'
205
+ when KEY_MAP[:digit_3]
117
206
  self.number_to_move += '3'
118
207
  redraw = false
119
- when '4'
208
+ when KEY_MAP[:digit_4]
120
209
  self.number_to_move += '4'
121
210
  redraw = false
122
- when '5'
211
+ when KEY_MAP[:digit_5]
123
212
  self.number_to_move += '5'
124
213
  redraw = false
125
- when '6'
214
+ when KEY_MAP[:digit_6]
126
215
  self.number_to_move += '6'
127
216
  redraw = false
128
- when '7'
217
+ when KEY_MAP[:digit_7]
129
218
  self.number_to_move += '7'
130
219
  redraw = false
131
- when '8'
220
+ when KEY_MAP[:digit_8]
132
221
  self.number_to_move += '8'
133
222
  redraw = false
134
- when '9'
223
+ when KEY_MAP[:digit_9]
135
224
  self.number_to_move += '9'
136
225
  redraw = false
137
- when '0'
226
+ when KEY_MAP[:digit_0]
138
227
  if number_to_move[0] # You can append zeros to existing number_to_move...
139
228
  self.number_to_move += '0'
140
229
  redraw = false
141
230
  else # ...but otherwise it's understood to be a view||cursor reset.
142
231
  reset_the_view_or_cursor
143
232
  end
144
- when 'A' # Up arrow
145
- action_count = !number_to_move.empty? ? number_to_move.to_i : 1
146
- navigate_up_multiple(action_count)
147
-
148
- self.number_to_move = ''
149
- tree_view.slide_view_to_cursor
150
- when 'B' # Down arrow
151
- action_count = !number_to_move.empty? ? number_to_move.to_i : 1
152
- navigate_down_multiple(action_count)
153
-
154
- self.number_to_move = ''
155
- tree_view.slide_view_to_cursor
156
- when 'C' # Right arrow
233
+
234
+ when KEY_MAP[:move_cursor_up]
235
+ move_cursor_up_action
236
+ when KEY_MAP[:move_cursor_down]
237
+ move_cursor_down_action
238
+ when KEY_MAP[:open_bucket]
239
+ expand_targets
240
+ when KEY_MAP[:close_bucket]
241
+ collapse_targets
242
+
243
+ when KEY_MAP[:homerow_move_cursor_up]
244
+ move_cursor_up_action
245
+ when KEY_MAP[:homerow_move_cursor_up_fast]
246
+ move_cursor_up_action(12) # 12 matches the digits provided by shift+up
247
+ when KEY_MAP[:homerow_move_cursor_down]
248
+ move_cursor_down_action
249
+ when KEY_MAP[:homerow_move_cursor_down_fast]
250
+ move_cursor_down_action(12) # 12 matches the digits provided by shift+down
251
+ when KEY_MAP[:homerow_open_bucket]
157
252
  expand_targets
158
- when 'D' # Left arrow
253
+ when KEY_MAP[:homerow_close_bucket]
159
254
  collapse_targets
160
- when ' '
255
+
256
+ when KEY_MAP[:toggle_view_panel]
161
257
  toggle_view_panel
162
- when 'l'
258
+ when KEY_MAP[:switch_lens]
163
259
  scroll_lens_type
164
- when 'L'
260
+ when KEY_MAP[:switch_subject_type]
165
261
  toggle_current_subject_type
166
- when 'w'
262
+
263
+ when KEY_MAP[:move_view_up]
167
264
  current_view_panel.move_view_up(5)
168
- when 's'
265
+ when KEY_MAP[:move_view_down]
169
266
  current_view_panel.move_view_down(5)
170
- when 'a'
267
+ when KEY_MAP[:move_view_left]
171
268
  current_view_panel.move_view_left(5)
172
- when 'd'
269
+ when KEY_MAP[:move_view_right]
173
270
  current_view_panel.move_view_right(5)
174
- when '∑' # Alt+w
271
+
272
+ when KEY_MAP[:move_view_up_fast]
175
273
  current_view_panel.move_view_up(50)
176
- when 'ß' # Alt+s
274
+ when KEY_MAP[:move_view_down_fast]
177
275
  current_view_panel.move_view_down(50)
178
- when 'å' # Alt+a
276
+ when KEY_MAP[:move_view_left_fast]
179
277
  current_view_panel.move_view_left(50)
180
- when '∂' # Alt+d
278
+ when KEY_MAP[:move_view_right_fast]
181
279
  current_view_panel.move_view_right(50)
182
- when '?'
183
- in_scry_session = run_help_screen_ui
184
- when '@'
280
+
281
+ when KEY_MAP[:build_instance_variables]
185
282
  build_instance_variables_for_target_ros
186
283
  tree_view.slide_view_to_cursor # Just a nice-to-have
187
- when '.'
284
+ when KEY_MAP[:build_ar_relations]
188
285
  build_activerecord_relations_for_target_ros
189
286
  tree_view.slide_view_to_cursor # Just a nice-to-have
190
- when '('
287
+ when KEY_MAP[:build_enum_children]
191
288
  build_enum_children_for_target_ros
192
289
  tree_view.slide_view_to_cursor # Just a nice-to-have
193
- when '|'
290
+ when KEY_MAP[:smart_open]
291
+ smart_open_target_ros
292
+ tree_view.slide_view_to_cursor # Just a nice-to-have
293
+
294
+ when KEY_MAP[:select_siblings]
194
295
  sibling_ros = if current_ro.top_ro?
195
296
  [top_ro]
196
297
  else
197
- current_ro.parent_ro.sub_ros.dup # If we don't dup,
298
+ current_ro.parent_ro.sub_ros.dup
299
+ # ^If we don't dup,
198
300
  # then '-' can remove ros from `sub_ros`.
199
301
  end
200
302
  if special_command_targets.sort == sibling_ros.sort
@@ -202,61 +304,126 @@ class Scryglass::Session
202
304
  else
203
305
  self.special_command_targets = sibling_ros
204
306
  end
205
- when '*'
206
- all_the_ros = all_ros.dup # If we don't dup,
307
+ when KEY_MAP[:select_all]
308
+ all_the_ros = all_ros.dup
309
+ # ^If we don't dup,
207
310
  # then '-' can remove ros from all_ros.
208
311
  if special_command_targets.sort == all_the_ros.sort
209
312
  self.special_command_targets = []
210
313
  else
211
314
  self.special_command_targets = all_the_ros
212
315
  end
213
- when '-'
316
+ when KEY_MAP[:select_current]
214
317
  if special_command_targets.include?(current_ro)
215
318
  special_command_targets.delete(current_ro)
216
319
  else
217
320
  special_command_targets << current_ro
218
321
  end
219
- when '/'
220
- _screen_height, screen_width = $stdout.winsize
221
- $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
222
- $stdout.print ' ' * screen_width
223
- $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
224
- $stdout.print SEARCH_PROMPT
225
- $stdout.write "#{CSI}1;#{SEARCH_PROMPT.ansiless_length + 1}H" # (Moves
226
- # console cursor to just after the search prompt, before user types)
227
- query = $stdin.gets.chomp
228
- unless query.empty?
229
- self.last_search = query
230
- go_to_next_search_result
231
- end
232
- when 'n'
322
+
323
+ when KEY_MAP[:start_search]
324
+ initiate_search
325
+ when KEY_MAP[:continue_search]
233
326
  if last_search
234
327
  go_to_next_search_result
235
328
  else
236
- $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
237
- $stdout.write "\e[7m-- No Search has been entered --\e[00m"
238
- sleep 2
329
+ message = { text: 'No Search has been entered', end_time: Time.now + 2 }
330
+ self.current_warning_messages << message
239
331
  end
240
- when "\r" # [ENTER]
241
- visually_close_ui
242
- return subjects_of_target_ros
332
+
333
+ when KEY_MAP[:change_session_right]
334
+ self.signal_to_manager = :change_session_right
335
+ return
336
+ when KEY_MAP[:change_session_left]
337
+ self.signal_to_manager = :change_session_left
338
+ return
339
+ when KEY_MAP[:name_objects]
340
+ name_subjects_of_target_ros
341
+ when KEY_MAP[:return_objects]
342
+ self.signal_to_manager = :return
343
+ subjects = subjects_of_target_ros
344
+ self.special_command_targets = []
345
+ return subjects
243
346
  end
244
347
 
245
- print "\a" if Time.now - wait_start_time > 4 && last_keypress != '?' # (Audio 'beep')
348
+ beep_if_user_had_to_wait(wait_start_time)
246
349
  end
247
350
  end
248
351
 
249
- def top_ro
250
- all_ros.first
352
+ def set_console_cursor_below_content(floor_the_cursor:)
353
+ if floor_the_cursor
354
+ screen_height, _screen_width = $stdout.winsize
355
+ $stdout.write "#{CSI}#{screen_height};1H\n" # (Moves console cursor to bottom left corner, then one more)
356
+ return
357
+ end
358
+
359
+ bare_screen_string =
360
+ current_view_panel.visible_header_string + "\n" +
361
+ current_view_panel.visible_body_string
362
+ split_lines = bare_screen_string.split("\n")
363
+ rows_filled = split_lines.count
364
+ $stdout.write "#{CSI}#{rows_filled};1H\n" # Moves console cursor to bottom
365
+ # of *content*, then one more.
251
366
  end
252
367
 
253
- def last_keypress
254
- last_two_signals = user_signals.last(2)
255
- last_two_signals.last || last_two_signals.first
368
+ def tab_string
369
+ top_ro_preview = top_ro.value_string
370
+ tab = if session_is_current
371
+ "\e[7m #{tab_icon}: #{top_ro_preview} \e[00m"
372
+ else
373
+ " \e[7m#{tab_icon}:\e[00m #{top_ro_preview} "
374
+ end
375
+ tab
376
+ end
377
+
378
+ def subjects_of_target_ros
379
+ if special_command_targets.any?
380
+ return special_command_targets.map(&:current_subject)
381
+ end
382
+
383
+ current_ro.current_subject
256
384
  end
257
385
 
258
386
  private
259
387
 
388
+ def beep_if_user_had_to_wait(wait_start_time)
389
+ patient_keys = KEY_MAP.slice(*PATIENT_ACTIONS).values
390
+ user_has_waited_at_least_four_seconds =
391
+ Time.now - wait_start_time > 4 &&
392
+ !patient_keys.include?(last_keypress)
393
+ print "\a" if user_has_waited_at_least_four_seconds # (Audio 'beep')
394
+ end
395
+
396
+ def initiate_search
397
+ _screen_height, screen_width = $stdout.winsize
398
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
399
+ $stdout.print ' ' * screen_width
400
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
401
+ $stdout.print SEARCH_PROMPT
402
+ $stdout.write "#{CSI}1;#{SEARCH_PROMPT.ansiless_length + 1}H" # (Moves
403
+ # console cursor to just after the search prompt, before user types)
404
+ query = $stdin.gets.chomp
405
+ unless query.empty?
406
+ self.last_search = query
407
+ go_to_next_search_result
408
+ end
409
+ end
410
+
411
+ def move_cursor_up_action(action_count = nil)
412
+ action_count ||= !number_to_move.empty? ? number_to_move.to_i : 1
413
+ navigate_up_multiple(action_count)
414
+
415
+ self.number_to_move = ''
416
+ tree_view.slide_view_to_cursor
417
+ end
418
+
419
+ def move_cursor_down_action(action_count = nil)
420
+ action_count ||= !number_to_move.empty? ? number_to_move.to_i : 1
421
+ navigate_down_multiple(action_count)
422
+
423
+ self.number_to_move = ''
424
+ tree_view.slide_view_to_cursor
425
+ end
426
+
260
427
  def clear_tracked_values
261
428
  self.special_command_targets = []
262
429
  self.last_search = nil
@@ -270,6 +437,27 @@ class Scryglass::Session
270
437
  print bar unless bar.tr(' ', '').empty?
271
438
  end
272
439
 
440
+ def print_current_warning_messages
441
+ return if current_warning_messages.empty?
442
+
443
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
444
+ wing = ' ' * 3
445
+
446
+ self.current_warning_messages.reject! { |message| Time.now > message[:end_time] }
447
+ messages = current_warning_messages.map { |message| message[:text] }
448
+ print messages.map { |message| "\e[7m#{wing + message + wing}\e[00m" }.join("\n")
449
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
450
+ end
451
+
452
+ def print_session_tabs_bar_if_changed
453
+ seconds_in_tab = Time.now - session_view_start_time
454
+ if seconds_in_tab < 2
455
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
456
+ print session_manager.session_tabs_bar
457
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
458
+ end
459
+ end
460
+
273
461
  def current_view_panel
274
462
  view_panels[current_panel_type]
275
463
  end
@@ -282,14 +470,6 @@ class Scryglass::Session
282
470
  view_panels[:lens]
283
471
  end
284
472
 
285
- def colorize(screen_string)
286
- dot = '•'
287
- cyan_dot = "\e[36m#{dot}\e[00m" # cyan then back to *default*
288
- screen_string.gsub!('•', cyan_dot)
289
-
290
- screen_string
291
- end
292
-
293
473
  def display_active_searching_indicator
294
474
  $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
295
475
  message = ' Searching... '
@@ -332,13 +512,8 @@ class Scryglass::Session
332
512
  tree_view.current_view_coords = { y: 0, x: 0 }
333
513
  tree_view.slide_view_to_cursor
334
514
  else
335
- $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
336
- message = ' No Match Found '
337
- pad = SEARCH_PROMPT.length - message.length
338
- wing = '-' * (pad / 2)
339
-
340
- $stdout.write "\e[7m#{wing + message + wing}\e[00m"
341
- sleep 2
515
+ message = { text: 'No Match Found', end_time: Time.now + 2 }
516
+ self.current_warning_messages << message
342
517
  end
343
518
  end
344
519
 
@@ -346,7 +521,7 @@ class Scryglass::Session
346
521
  previous_signal = user_signals.last
347
522
  new_signal =
348
523
  begin
349
- Timeout.timeout(0.1) { $stdin.getch }
524
+ Timeout.timeout(0.3) { $stdin.getch }
350
525
  rescue Timeout::Error
351
526
  nil
352
527
  end
@@ -381,16 +556,17 @@ class Scryglass::Session
381
556
  new_signal = fetch_user_signal
382
557
 
383
558
  case new_signal
384
- when 'esc'
559
+ when nil
560
+ when KEY_MAP[:escape]
385
561
  return true
386
- when '?'
562
+ when KEY_MAP[:control_screen]
387
563
  current_help_screen_index += 1
388
- when 'q'
564
+ when KEY_MAP[:quit_session]
389
565
  $stdout.write "#{CSI}#{screen_height};1H" # (Moves console cursor to
390
566
  # bottom left corner). This helps 'q' not print the console prompt at
391
567
  # the top of the screen, overlapping with the old display.
392
568
  return false
393
- when "\u0003"
569
+ when KEY_MAP[:ctrl_c]
394
570
  screen_height, _screen_width = $stdout.winsize
395
571
  puts "\n" * screen_height
396
572
  raise IRB::Abort, 'Ctrl+C Detected'
@@ -446,37 +622,68 @@ class Scryglass::Session
446
622
  current_view_panel.ensure_correct_view_coords
447
623
  screen_string = current_view_panel.screen_string
448
624
 
449
- screen_string = colorize(screen_string) if Scryglass.config.dot_coloring
450
625
  Hexes.overwrite_screen(screen_string)
451
626
  $stdout.write "#{CSI}1;1H" # Moves terminal cursor to top left corner,
452
627
  # mostly for consistency.
628
+ print_current_warning_messages
629
+ print_session_tabs_bar_if_changed
453
630
  end
454
631
 
455
- def set_console_cursor_below_content
456
- bare_screen_string =
457
- current_view_panel.visible_header_string + "\n" +
458
- current_view_panel.visible_body_string
459
- split_lines = bare_screen_string.split("\n")
460
- rows_filled = split_lines.count
461
- $stdout.write "#{CSI}#{rows_filled};1H\n" # Moves console cursor to bottom
462
- # of *content*, then one more.
463
- end
464
-
465
- def visually_close_ui
632
+ def get_subject_name_from_user
466
633
  _screen_height, screen_width = $stdout.winsize
467
- set_console_cursor_below_content
468
- puts '·' * screen_width, "\n"
469
- puts SESSION_CLOSED_MESSAGE
470
- end
634
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
635
+ $stdout.print ' ' * screen_width
636
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
637
+ $stdout.print VARNAME_PROMPT
638
+ $stdout.write "#{CSI}1;#{VARNAME_PROMPT.ansiless_length + 1}H" # (Moves
639
+ # console cursor to just after the varname prompt, before user types)
640
+ $stdin.gets.chomp
641
+ end
642
+
643
+ def name_subjects_of_target_ros
644
+ typed_name = get_subject_name_from_user
645
+ typed_name = typed_name.tr(' ', '')
646
+
647
+ if typed_name.empty?
648
+ message = { text: 'Instance Variable name cannot be blank',
649
+ end_time: Time.now + 2 }
650
+ self.current_warning_messages << message
651
+ print "\a" # (Audio 'beep')
652
+ return
653
+ end
471
654
 
472
- def subjects_of_target_ros
473
- if special_command_targets.any?
474
- return_targets = special_command_targets
475
- self.special_command_targets = []
476
- return return_targets.map(&:current_subject)
655
+ current_console_binding = session_manager.current_console_binding
656
+ preexisting_iv_names = current_console_binding
657
+ .eval('instance_variables') # Different than just `.instance_variables`
658
+ .map { |iv| iv.to_s.tr('@', '') }
659
+ all_method_names = preexisting_iv_names |
660
+ current_console_binding.methods |
661
+ current_console_binding.singleton_methods |
662
+ current_console_binding.private_methods
663
+ conflicting_method_name = all_method_names.find do |method_name|
664
+ pure_method_name = method_name.to_s.tr('=', '')
665
+ typed_name == pure_method_name
477
666
  end
478
667
 
479
- current_ro.current_subject
668
+ if conflicting_method_name
669
+ message = { text: 'Instance Variable name conflict',
670
+ end_time: Time.now + 2 }
671
+ self.current_warning_messages << message
672
+ print "\a" # (Audio 'beep')
673
+ return
674
+ end
675
+
676
+ set_iv_name_in_console =
677
+ "@#{typed_name} = " \
678
+ "$scry_session_manager.current_session.subjects_of_target_ros"
679
+ current_console_binding.eval(set_iv_name_in_console)
680
+ session_manager.current_binding_tracker.user_named_variables << "@#{typed_name}"
681
+
682
+ message = { text: "#{subjects_of_target_ros.class} assigned to: @#{typed_name}",
683
+ end_time: Time.now + 2 }
684
+ self.current_warning_messages << message
685
+
686
+ self.special_command_targets = []
480
687
  end
481
688
 
482
689
  def navigate_up_multiple(action_count)