scryglass 0.1.0 → 2.0.1

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
@@ -114,19 +123,15 @@ module Scryglass
114
123
 
115
124
  # (Used for recalculate_indeces after new Ros have been injected)
116
125
  def next_ro_without_using_index
117
- if sub_ros.any?
118
- sub_ros.first
119
- elsif top_ro?
120
- nil
121
- elsif sibling_down.present?
122
- sibling_down
123
- else
124
- upward_feeler_ro = self
125
- until upward_feeler_ro.sibling_down.present? || upward_feeler_ro.top_ro?
126
- upward_feeler_ro = upward_feeler_ro.parent_ro
127
- end
128
- upward_feeler_ro.sibling_down
126
+ return sub_ros.first if sub_ros.first
127
+ return nil if top_ro?
128
+ return sibling_down if sibling_down
129
+
130
+ upward_feeler_ro = self
131
+ until upward_feeler_ro.sibling_down || upward_feeler_ro.top_ro?
132
+ upward_feeler_ro = upward_feeler_ro.parent_ro
129
133
  end
134
+ upward_feeler_ro.sibling_down
130
135
  end
131
136
 
132
137
  def sibling_down
@@ -172,12 +177,12 @@ module Scryglass
172
177
  !!key_value_relationship_indicator
173
178
  end
174
179
 
175
- private
176
-
177
180
  def special_sub_ros
178
181
  sub_ros.select(&:special_sub_ro_type)
179
182
  end
180
183
 
184
+ private
185
+
181
186
  def normal_sub_ros
182
187
  sub_ros.reject(&:special_sub_ro_type)
183
188
  end
@@ -191,7 +196,9 @@ module Scryglass
191
196
  # communicating with a solid preexisting symbol), but keeping the idea here:
192
197
  # sub_ros_order_of_magnitude = normal_sub_ros.count.to_s.length
193
198
  # wrappers.dup.insert(1, '•' * sub_ros_order_of_magnitude)
194
- 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)
195
202
  else
196
203
  wrappers
197
204
  end
@@ -218,20 +225,78 @@ module Scryglass
218
225
  (tab_length * depth) + consistent_margin
219
226
  end
220
227
 
228
+ def cursor_char
229
+ Scryglass::Session::CURSOR_CHARACTER
230
+ end
231
+
221
232
  def cursor_string
222
- 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
223
238
 
224
- if nugget? && has_cursor && value.is_a?(Enumerable) &&
225
- value.any? &&
226
- enum_sub_ros.empty?
227
- cursor[0] = '('
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?
228
251
  end
229
252
 
230
- if value.instance_variables.any? && iv_sub_ros.empty?
231
- cursor[1] = '@'
253
+ return 'X' if enum_check.nil?
254
+
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?
232
265
  end
233
266
 
234
- cursor
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?
281
+ end
282
+
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
235
300
  end
236
301
  end
237
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)
@@ -310,7 +385,7 @@ module Scryglass
310
385
  through_indicator = is_through ? '(t)' : ' '
311
386
  end
312
387
 
313
- is_scoped = info.scope.present?
388
+ is_scoped = !!info.scope
314
389
  if include_scoped_associations
315
390
  scoped_indicator = is_scoped ? '(s)' : ' '
316
391
  end
@@ -322,7 +397,7 @@ module Scryglass
322
397
  relation_name.to_s
323
398
  end
324
399
 
325
- if ar_value.present? || 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
- attr_accessor :user_input, :last_search, :number_to_move
14
+ attr_accessor :user_signals, :last_search, :number_to_move
15
+
16
+ attr_accessor :session_manager, :signal_to_manager, :session_is_current,
17
+ :tab_icon, :session_view_start_time
15
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
@@ -33,8 +96,14 @@ class Scryglass::Session
33
96
  self.current_panel_type = :tree
34
97
  self.special_command_targets = []
35
98
  self.number_to_move = ''
36
- self.user_input = nil
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,150 +117,186 @@ class Scryglass::Session
48
117
  }
49
118
  end
50
119
 
51
- def run_scry_ui(actions:)
52
- in_scry_session = true
53
- redraw = true
120
+ def top_ro
121
+ all_ros.first
122
+ end
54
123
 
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
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
130
+ redraw = true
131
+ signal_to_manager = nil
132
+ self.session_view_start_time = Time.now # For this particular tab/session
133
+
134
+ ## On hold: Record/Playback Functionality:
135
+ # case actions
136
+ # when :record
137
+ # $scry_session_actions_performed = []
138
+ # when :playback
139
+ # if $scry_session_actions_performed.blank?
140
+ # raise 'Could not find recording of previous session\'s actions'
141
+ # end
142
+ # @input_stack = $scry_session_actions_performed.dup
143
+ # end
64
144
 
65
145
  # We print a full screen of lines so the first call of draw_screen doesn't
66
146
  # write over any previous valuable content the user had in the console.
67
147
  print Hexes.opacify_screen_string(Hexes.simple_screen_slice(boot_screen))
68
148
 
69
- while in_scry_session
149
+ while true
70
150
  draw_screen if redraw
71
151
  redraw = true
72
152
 
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
153
+ ## On hold: Record/Playback Functionality:
154
+ # case actions
155
+ # when :record
156
+ # self.user_input = $stdin.getch
157
+ # $scry_session_actions_performed << user_input
158
+ # when :playback
159
+ # if @input_stack.any? # (IV to be easily accessible for debugging)
160
+ # self.user_input = @input_stack.shift
161
+ # sleep 0.05
162
+ # else
163
+ # self.user_input = $stdin.getch
164
+ # end
165
+ # else
166
+ # self.user_input = $stdin.getch
167
+ # end
168
+
169
+ new_signal = fetch_user_signal
87
170
 
88
171
  wait_start_time = Time.now
89
172
 
90
- case user_input
91
- when "\u0003"
173
+ case new_signal
174
+ when nil
175
+ when KEY_MAP[:escape]
176
+ case current_panel_type
177
+ when :lens
178
+ self.current_panel_type = :tree
179
+ when :tree
180
+ clear_tracked_values
181
+ end
182
+ when KEY_MAP[:ctrl_c]
92
183
  set_console_cursor_below_content
93
184
  raise IRB::Abort, 'Ctrl+C Detected'
94
- when 'q'
95
- in_scry_session = false
96
- visually_close_ui
97
- 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]
98
198
  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'
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]
102
203
  self.number_to_move += '2'
103
204
  redraw = false
104
- when '3'
205
+ when KEY_MAP[:digit_3]
105
206
  self.number_to_move += '3'
106
207
  redraw = false
107
- when '4'
208
+ when KEY_MAP[:digit_4]
108
209
  self.number_to_move += '4'
109
210
  redraw = false
110
- when '5'
211
+ when KEY_MAP[:digit_5]
111
212
  self.number_to_move += '5'
112
213
  redraw = false
113
- when '6'
214
+ when KEY_MAP[:digit_6]
114
215
  self.number_to_move += '6'
115
216
  redraw = false
116
- when '7'
217
+ when KEY_MAP[:digit_7]
117
218
  self.number_to_move += '7'
118
219
  redraw = false
119
- when '8'
220
+ when KEY_MAP[:digit_8]
120
221
  self.number_to_move += '8'
121
222
  redraw = false
122
- when '9'
223
+ when KEY_MAP[:digit_9]
123
224
  self.number_to_move += '9'
124
225
  redraw = false
125
- when '0'
126
- if number_to_move.present? # You can append zeros to number_to_move...
226
+ when KEY_MAP[:digit_0]
227
+ if number_to_move[0] # You can append zeros to existing number_to_move...
127
228
  self.number_to_move += '0'
128
229
  redraw = false
129
230
  else # ...but otherwise it's understood to be a view||cursor reset.
130
231
  reset_the_view_or_cursor
131
232
  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
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]
147
239
  expand_targets
148
- when 'D' # Left arrow
240
+ when KEY_MAP[:close_bucket]
149
241
  collapse_targets
150
- lens_view.recalculate_boundaries if current_panel_type == :lens
151
- when ' '
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]
252
+ expand_targets
253
+ when KEY_MAP[:homerow_close_bucket]
254
+ collapse_targets
255
+
256
+ when KEY_MAP[:toggle_view_panel]
152
257
  toggle_view_panel
153
- lens_view.recalculate_boundaries if current_panel_type == :lens
154
- when 'l'
258
+ when KEY_MAP[:switch_lens]
155
259
  scroll_lens_type
156
- lens_view.recalculate_boundaries if current_panel_type == :lens
157
- when 'L'
260
+ when KEY_MAP[:switch_subject_type]
158
261
  toggle_current_subject_type
159
- lens_view.recalculate_boundaries if current_panel_type == :lens
160
- when 'w'
262
+
263
+ when KEY_MAP[:move_view_up]
161
264
  current_view_panel.move_view_up(5)
162
- when 's'
265
+ when KEY_MAP[:move_view_down]
163
266
  current_view_panel.move_view_down(5)
164
- when 'a'
267
+ when KEY_MAP[:move_view_left]
165
268
  current_view_panel.move_view_left(5)
166
- when 'd'
269
+ when KEY_MAP[:move_view_right]
167
270
  current_view_panel.move_view_right(5)
168
- when '∑' # Alt+w
271
+
272
+ when KEY_MAP[:move_view_up_fast]
169
273
  current_view_panel.move_view_up(50)
170
- when 'ß' # Alt+s
274
+ when KEY_MAP[:move_view_down_fast]
171
275
  current_view_panel.move_view_down(50)
172
- when 'å' # Alt+a
276
+ when KEY_MAP[:move_view_left_fast]
173
277
  current_view_panel.move_view_left(50)
174
- when '∂' # Alt+d
278
+ when KEY_MAP[:move_view_right_fast]
175
279
  current_view_panel.move_view_right(50)
176
- when '?'
177
- in_scry_session = run_help_screen_ui
178
- when '@'
280
+
281
+ when KEY_MAP[:build_instance_variables]
179
282
  build_instance_variables_for_target_ros
180
- tree_view.recalculate_boundaries
181
283
  tree_view.slide_view_to_cursor # Just a nice-to-have
182
- when '.'
284
+ when KEY_MAP[:build_ar_relations]
183
285
  build_activerecord_relations_for_target_ros
184
- tree_view.recalculate_boundaries
185
286
  tree_view.slide_view_to_cursor # Just a nice-to-have
186
- when '('
287
+ when KEY_MAP[:build_enum_children]
187
288
  build_enum_children_for_target_ros
188
- tree_view.recalculate_boundaries
189
289
  tree_view.slide_view_to_cursor # Just a nice-to-have
190
- 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]
191
295
  sibling_ros = if current_ro.top_ro?
192
296
  [top_ro]
193
297
  else
194
- 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,
195
300
  # then '-' can remove ros from `sub_ros`.
196
301
  end
197
302
  if special_command_targets.sort == sibling_ros.sort
@@ -199,58 +304,158 @@ class Scryglass::Session
199
304
  else
200
305
  self.special_command_targets = sibling_ros
201
306
  end
202
- when '*'
203
- 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,
204
310
  # then '-' can remove ros from all_ros.
205
311
  if special_command_targets.sort == all_the_ros.sort
206
312
  self.special_command_targets = []
207
313
  else
208
314
  self.special_command_targets = all_the_ros
209
315
  end
210
- when '-'
316
+ when KEY_MAP[:select_current]
211
317
  if special_command_targets.include?(current_ro)
212
318
  special_command_targets.delete(current_ro)
213
319
  else
214
320
  special_command_targets << current_ro
215
321
  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'
322
+
323
+ when KEY_MAP[:start_search]
324
+ initiate_search
325
+ when KEY_MAP[:continue_search]
227
326
  if last_search
228
327
  go_to_next_search_result
229
328
  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
329
+ message = { text: 'No Search has been entered', end_time: Time.now + 2 }
330
+ self.current_warning_messages << message
233
331
  end
234
- when "\r" # [ENTER]
235
- visually_close_ui
236
- 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
237
346
  end
238
347
 
239
- print "\a" if Time.now - wait_start_time > 4 && user_input != '?' # (Audio 'beep')
348
+ beep_if_user_had_to_wait(wait_start_time)
240
349
  end
241
350
  end
242
351
 
243
- def top_ro
244
- 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.
366
+ end
367
+
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
245
384
  end
246
385
 
247
386
  private
248
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
+
427
+ def clear_tracked_values
428
+ self.special_command_targets = []
429
+ self.last_search = nil
430
+ self.number_to_move = ''
431
+ end
432
+
249
433
  def print_progress_bar
250
434
  screen_height, _screen_width = $stdout.winsize
251
435
  bar = progress_bar.to_s
252
436
  $stdout.write "#{CSI}#{screen_height};1H" # (Moves console cursor to bottom left corner)
253
- print bar if bar.present?
437
+ print bar unless bar.tr(' ', '').empty?
438
+ end
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
254
459
  end
255
460
 
256
461
  def current_view_panel
@@ -265,14 +470,6 @@ class Scryglass::Session
265
470
  view_panels[:lens]
266
471
  end
267
472
 
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
473
  def display_active_searching_indicator
277
474
  $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
278
475
  message = ' Searching... '
@@ -315,14 +512,32 @@ class Scryglass::Session
315
512
  tree_view.current_view_coords = { y: 0, x: 0 }
316
513
  tree_view.slide_view_to_cursor
317
514
  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)
515
+ message = { text: 'No Match Found', end_time: Time.now + 2 }
516
+ self.current_warning_messages << message
517
+ end
518
+ end
519
+
520
+ def fetch_user_signal
521
+ previous_signal = user_signals.last
522
+ new_signal =
523
+ begin
524
+ Timeout.timeout(0.3) { $stdin.getch }
525
+ rescue Timeout::Error
526
+ nil
527
+ end
322
528
 
323
- $stdout.write "\e[7m#{wing + message + wing}\e[00m"
324
- sleep 2
529
+ ## Since many keys, including arrow keys, result in several signals being
530
+ ## sent (e.g. DOWN: "\e" then "[" then "B" in RAPID succession), the
531
+ ## *pause* after a genuine escape key press (also "\e") is the only way
532
+ ## to distinguish it precisely.
533
+ genuine_escape_key_press = new_signal.nil? && previous_signal == "\e"
534
+ if genuine_escape_key_press
535
+ new_signal = 'esc'
325
536
  end
537
+
538
+ user_signals << new_signal unless new_signal.nil? && previous_signal.nil?
539
+
540
+ new_signal
326
541
  end
327
542
 
328
543
  def run_help_screen_ui
@@ -337,17 +552,21 @@ class Scryglass::Session
337
552
  sliced_help_screen = Hexes.simple_screen_slice(current_help_screen)
338
553
  help_screen_string = Hexes.opacify_screen_string(sliced_help_screen)
339
554
  Hexes.overwrite_screen(help_screen_string)
340
- help_screen_user_input = $stdin.getch
341
555
 
342
- case help_screen_user_input
343
- when '?'
556
+ new_signal = fetch_user_signal
557
+
558
+ case new_signal
559
+ when nil
560
+ when KEY_MAP[:escape]
561
+ return true
562
+ when KEY_MAP[:control_screen]
344
563
  current_help_screen_index += 1
345
- when 'q'
564
+ when KEY_MAP[:quit_session]
346
565
  $stdout.write "#{CSI}#{screen_height};1H" # (Moves console cursor to
347
566
  # bottom left corner). This helps 'q' not print the console prompt at
348
567
  # the top of the screen, overlapping with the old display.
349
568
  return false
350
- when "\u0003"
569
+ when KEY_MAP[:ctrl_c]
351
570
  screen_height, _screen_width = $stdout.winsize
352
571
  puts "\n" * screen_height
353
572
  raise IRB::Abort, 'Ctrl+C Detected'
@@ -375,7 +594,6 @@ class Scryglass::Session
375
594
 
376
595
  move_cursor_to(current_ro.parent_ro) until current_ro.visible?
377
596
  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
597
  end
380
598
 
381
599
  def expand_targets
@@ -388,7 +606,6 @@ class Scryglass::Session
388
606
  else
389
607
  expand!(current_ro)
390
608
  end
391
- tree_view.recalculate_boundaries
392
609
  end
393
610
 
394
611
  def reset_the_view_or_cursor
@@ -400,40 +617,73 @@ class Scryglass::Session
400
617
  end
401
618
 
402
619
  def draw_screen
620
+ current_view_panel.recalculate_boundaries # This now happens at every screen
621
+ # draw to account for the user changing the screen size. Otherwise glitch.
403
622
  current_view_panel.ensure_correct_view_coords
404
623
  screen_string = current_view_panel.screen_string
405
624
 
406
- screen_string = colorize(screen_string) if Scryglass.config.dot_coloring
407
625
  Hexes.overwrite_screen(screen_string)
408
626
  $stdout.write "#{CSI}1;1H" # Moves terminal cursor to top left corner,
409
627
  # mostly for consistency.
628
+ print_current_warning_messages
629
+ print_session_tabs_bar_if_changed
410
630
  end
411
631
 
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
632
+ def get_subject_name_from_user
423
633
  _screen_height, screen_width = $stdout.winsize
424
- set_console_cursor_below_content
425
- puts '·' * screen_width, "\n"
426
- puts SESSION_CLOSED_MESSAGE
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
427
641
  end
428
642
 
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)
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
434
653
  end
435
654
 
436
- current_ro.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
666
+ end
667
+
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 + 3 }
684
+ self.current_warning_messages << message
685
+
686
+ self.special_command_targets = []
437
687
  end
438
688
 
439
689
  def navigate_up_multiple(action_count)