scryglass 1.1.0 → 2.0.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.
@@ -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)