lib/scryglass/session.rb in scryglass-1.1.0 vs lib/scryglass/session.rb in scryglass-2.0.0
- old
+ new
@@ -7,36 +7,105 @@
attr_accessor :all_ros, :current_ro, :special_command_targets
attr_accessor :current_view_coords, :current_lens, :current_subject_type,
:view_panels, :current_panel_type,
- :progress_bar
+ :progress_bar, :current_warning_messages
attr_accessor :user_signals, :last_search, :number_to_move
+ attr_accessor :session_manager, :signal_to_manager, :session_is_current,
+ :tab_icon, :session_view_start_time
+
CURSOR_CHARACTER = '–' # These are en dashes (alt+dash), not hyphens or em dashes.
- SEARCH_PROMPT = "\e[7mSearch for (regex, case-sensitive): /\e[00m"
+ SEARCH_PROMPT = "\e[7mSearch for (regex, case-sensitive): /\e[00m"
- SESSION_CLOSED_MESSAGE = '(Exited scry! Resume session with `scry` or `scry_resume`)'
+ VARNAME_PROMPT = "\e[7mName your object(s): @\e[00m"
SUBJECT_TYPES = [
:value,
:key
].freeze
CSI = "\e[" # "(C)ontrol (S)equence (I)ntroducer" for ANSI sequences
+ KEY_MAP = {
+ escape: 'esc', # Not a normal keystroke, see: genuine_escape_key_press
+ ctrl_c: "\u0003",
+ quit_session: 'q',
+ delete_session_tab: 'Q',
+ change_session_right: "\t", # Tab
+ change_session_left: 'Z', # Shift+Tab (well, one of its signals, after "\e" and "[")
+ digit_1: '1',
+ digit_2: '2',
+ digit_3: '3',
+ digit_4: '4',
+ digit_5: '5',
+ digit_6: '6',
+ digit_7: '7',
+ digit_8: '8',
+ digit_9: '9',
+ digit_0: '0',
+ move_cursor_up: 'A', # Up arrow (well, one of its signals, after "\e" and "[")
+ move_cursor_down: 'B', # Down arrow (well, one of its signals, after "\e" and "[")
+ open_bucket: 'C', # Right arrow (well, one of its signals, after "\e" and "[")
+ close_bucket: 'D', # Left arrow (well, one of its signals, after "\e" and "[")
+ homerow_move_cursor_up: 'k', # To be like VIM arrow keys
+ homerow_move_cursor_up_fast: 'K', # To be like VIM arrow keys
+ homerow_move_cursor_down: 'j', # To be like VIM arrow keys
+ homerow_move_cursor_down_fast: 'J', # To be like VIM arrow keys
+ homerow_open_bucket: 'l', # To be like VIM arrow keys
+ homerow_close_bucket: 'h', # To be like VIM arrow keys
+ # Note, shift-UP and shift-DOWN are not here, as those work very
+ # differently: by virtue of the type-a-number-first functionality.
+ toggle_view_panel: ' ',
+ switch_lens: '>',
+ switch_subject_type: '<',
+ move_view_up: 'w',
+ move_view_down: 's',
+ move_view_left: 'a',
+ move_view_right: 'd',
+ move_view_up_fast: '∑', # Alt+w
+ move_view_down_fast: 'ß', # Alt+s
+ move_view_left_fast: 'å', # Alt+a
+ move_view_right_fast: '∂', # Alt+d
+ control_screen: '?',
+ build_instance_variables: '@',
+ build_ar_relations: '.',
+ build_enum_children: '(',
+ smart_open: 'o',
+ select_siblings: '|',
+ select_all: '*',
+ select_current: '-',
+ start_search: '/',
+ continue_search: 'n',
+ return_objects: "\r", # [ENTER],
+ name_objects: "="
+ }.freeze
+
+ PATIENT_ACTIONS = [
+ :control_screen,
+ :escape,
+ :name_objects,
+ ].freeze
+
def initialize(seed)
self.all_ros = []
self.current_lens = 0
self.current_subject_type = :value
self.current_panel_type = :tree
self.special_command_targets = []
self.number_to_move = ''
self.user_signals = []
self.progress_bar = Prog::Pipe.new
+ self.current_warning_messages = []
+ self.session_manager = nil
+ self.signal_to_manager = nil
+ self.tab_icon = nil
+ self.session_is_current = false
+ self.session_view_start_time = nil
top_ro = roify(seed, parent_ro: nil, depth: 1)
top_ro.has_cursor = true
self.current_ro = top_ro
@@ -46,13 +115,23 @@
tree: Scryglass::TreePanel.new(scry_session: self),
lens: Scryglass::LensPanel.new(scry_session: self),
}
end
- def run_scry_ui(actions:)
- in_scry_session = true
+ def top_ro
+ all_ros.first
+ end
+
+ def last_keypress
+ last_two_signals = user_signals.last(2)
+ last_two_signals.last || last_two_signals.first
+ end
+
+ def run_scry_ui
redraw = true
+ signal_to_manager = nil
+ self.session_view_start_time = Time.now # For this particular tab/session
## On hold: Record/Playback Functionality:
# case actions
# when :record
# $scry_session_actions_performed = []
@@ -65,11 +144,11 @@
# We print a full screen of lines so the first call of draw_screen doesn't
# write over any previous valuable content the user had in the console.
print Hexes.opacify_screen_string(Hexes.simple_screen_slice(boot_screen))
- while in_scry_session
+ while true
draw_screen if redraw
redraw = true
## On hold: Record/Playback Functionality:
# case actions
@@ -91,174 +170,262 @@
wait_start_time = Time.now
case new_signal
when nil
- when 'esc'
+ when KEY_MAP[:escape]
case current_panel_type
when :lens
self.current_panel_type = :tree
when :tree
clear_tracked_values
end
- when "\u0003"
+ when KEY_MAP[:ctrl_c]
set_console_cursor_below_content
raise IRB::Abort, 'Ctrl+C Detected'
- when 'q'
- in_scry_session = false
- visually_close_ui
- when '1'
+ when KEY_MAP[:quit_session]
+ self.signal_to_manager = :quit
+ return
+ when KEY_MAP[:delete_session_tab]
+ self.signal_to_manager = :delete
+ return
+ when KEY_MAP[:control_screen]
+ remain_in_scry_session = run_help_screen_ui
+ unless remain_in_scry_session
+ self.signal_to_manager = :quit_from_help
+ return
+ end
+ when KEY_MAP[:digit_1]
self.number_to_move += '1'
- redraw = false # This allows you to type multi-digit number very
- # quickly and still have it process all the digits.
- when '2'
+ # This allows you to type multi-digit number very
+ # quickly and still have it process all the digits:
+ redraw = false
+ when KEY_MAP[:digit_2]
self.number_to_move += '2'
redraw = false
- when '3'
+ when KEY_MAP[:digit_3]
self.number_to_move += '3'
redraw = false
- when '4'
+ when KEY_MAP[:digit_4]
self.number_to_move += '4'
redraw = false
- when '5'
+ when KEY_MAP[:digit_5]
self.number_to_move += '5'
redraw = false
- when '6'
+ when KEY_MAP[:digit_6]
self.number_to_move += '6'
redraw = false
- when '7'
+ when KEY_MAP[:digit_7]
self.number_to_move += '7'
redraw = false
- when '8'
+ when KEY_MAP[:digit_8]
self.number_to_move += '8'
redraw = false
- when '9'
+ when KEY_MAP[:digit_9]
self.number_to_move += '9'
redraw = false
- when '0'
+ when KEY_MAP[:digit_0]
if number_to_move[0] # You can append zeros to existing number_to_move...
self.number_to_move += '0'
redraw = false
else # ...but otherwise it's understood to be a view||cursor reset.
reset_the_view_or_cursor
end
- when 'A' # Up arrow
- action_count = !number_to_move.empty? ? number_to_move.to_i : 1
- navigate_up_multiple(action_count)
- self.number_to_move = ''
- tree_view.slide_view_to_cursor
- when 'B' # Down arrow
- action_count = !number_to_move.empty? ? number_to_move.to_i : 1
- navigate_down_multiple(action_count)
+ when KEY_MAP[:move_cursor_up]
+ move_cursor_up_action
+ when KEY_MAP[:move_cursor_down]
+ move_cursor_down_action
+ when KEY_MAP[:open_bucket]
+ expand_targets
+ when KEY_MAP[:close_bucket]
+ collapse_targets
- self.number_to_move = ''
- tree_view.slide_view_to_cursor
- when 'C' # Right arrow
+ when KEY_MAP[:homerow_move_cursor_up]
+ move_cursor_up_action
+ when KEY_MAP[:homerow_move_cursor_up_fast]
+ move_cursor_up_action(12) # 12 matches the digits provided by shift+up
+ when KEY_MAP[:homerow_move_cursor_down]
+ move_cursor_down_action
+ when KEY_MAP[:homerow_move_cursor_down_fast]
+ move_cursor_down_action(12) # 12 matches the digits provided by shift+down
+ when KEY_MAP[:homerow_open_bucket]
expand_targets
- when 'D' # Left arrow
+ when KEY_MAP[:homerow_close_bucket]
collapse_targets
- when ' '
+
+ when KEY_MAP[:toggle_view_panel]
toggle_view_panel
- when 'l'
+ when KEY_MAP[:switch_lens]
scroll_lens_type
- when 'L'
+ when KEY_MAP[:switch_subject_type]
toggle_current_subject_type
- when 'w'
+
+ when KEY_MAP[:move_view_up]
current_view_panel.move_view_up(5)
- when 's'
+ when KEY_MAP[:move_view_down]
current_view_panel.move_view_down(5)
- when 'a'
+ when KEY_MAP[:move_view_left]
current_view_panel.move_view_left(5)
- when 'd'
+ when KEY_MAP[:move_view_right]
current_view_panel.move_view_right(5)
- when '∑' # Alt+w
+
+ when KEY_MAP[:move_view_up_fast]
current_view_panel.move_view_up(50)
- when 'ß' # Alt+s
+ when KEY_MAP[:move_view_down_fast]
current_view_panel.move_view_down(50)
- when 'å' # Alt+a
+ when KEY_MAP[:move_view_left_fast]
current_view_panel.move_view_left(50)
- when '∂' # Alt+d
+ when KEY_MAP[:move_view_right_fast]
current_view_panel.move_view_right(50)
- when '?'
- in_scry_session = run_help_screen_ui
- when '@'
+
+ when KEY_MAP[:build_instance_variables]
build_instance_variables_for_target_ros
tree_view.slide_view_to_cursor # Just a nice-to-have
- when '.'
+ when KEY_MAP[:build_ar_relations]
build_activerecord_relations_for_target_ros
tree_view.slide_view_to_cursor # Just a nice-to-have
- when '('
+ when KEY_MAP[:build_enum_children]
build_enum_children_for_target_ros
tree_view.slide_view_to_cursor # Just a nice-to-have
- when '|'
+ when KEY_MAP[:smart_open]
+ smart_open_target_ros
+ tree_view.slide_view_to_cursor # Just a nice-to-have
+
+ when KEY_MAP[:select_siblings]
sibling_ros = if current_ro.top_ro?
[top_ro]
else
- current_ro.parent_ro.sub_ros.dup # If we don't dup,
+ current_ro.parent_ro.sub_ros.dup
+ # ^If we don't dup,
# then '-' can remove ros from `sub_ros`.
end
if special_command_targets.sort == sibling_ros.sort
self.special_command_targets = []
else
self.special_command_targets = sibling_ros
end
- when '*'
- all_the_ros = all_ros.dup # If we don't dup,
+ when KEY_MAP[:select_all]
+ all_the_ros = all_ros.dup
+ # ^If we don't dup,
# then '-' can remove ros from all_ros.
if special_command_targets.sort == all_the_ros.sort
self.special_command_targets = []
else
self.special_command_targets = all_the_ros
end
- when '-'
+ when KEY_MAP[:select_current]
if special_command_targets.include?(current_ro)
special_command_targets.delete(current_ro)
else
special_command_targets << current_ro
end
- when '/'
- _screen_height, screen_width = $stdout.winsize
- $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
- $stdout.print ' ' * screen_width
- $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
- $stdout.print SEARCH_PROMPT
- $stdout.write "#{CSI}1;#{SEARCH_PROMPT.ansiless_length + 1}H" # (Moves
- # console cursor to just after the search prompt, before user types)
- query = $stdin.gets.chomp
- unless query.empty?
- self.last_search = query
- go_to_next_search_result
- end
- when 'n'
+
+ when KEY_MAP[:start_search]
+ initiate_search
+ when KEY_MAP[:continue_search]
if last_search
go_to_next_search_result
else
- $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
- $stdout.write "\e[7m-- No Search has been entered --\e[00m"
- sleep 2
+ message = { text: 'No Search has been entered', end_time: Time.now + 2 }
+ self.current_warning_messages << message
end
- when "\r" # [ENTER]
- visually_close_ui
- return subjects_of_target_ros
+
+ when KEY_MAP[:change_session_right]
+ self.signal_to_manager = :change_session_right
+ return
+ when KEY_MAP[:change_session_left]
+ self.signal_to_manager = :change_session_left
+ return
+ when KEY_MAP[:name_objects]
+ name_subjects_of_target_ros
+ when KEY_MAP[:return_objects]
+ self.signal_to_manager = :return
+ subjects = subjects_of_target_ros
+ self.special_command_targets = []
+ return subjects
end
- print "\a" if Time.now - wait_start_time > 4 && last_keypress != '?' # (Audio 'beep')
+ beep_if_user_had_to_wait(wait_start_time)
end
end
- def top_ro
- all_ros.first
+ def set_console_cursor_below_content(floor_the_cursor:)
+ if floor_the_cursor
+ screen_height, _screen_width = $stdout.winsize
+ $stdout.write "#{CSI}#{screen_height};1H\n" # (Moves console cursor to bottom left corner, then one more)
+ return
+ end
+
+ bare_screen_string =
+ current_view_panel.visible_header_string + "\n" +
+ current_view_panel.visible_body_string
+ split_lines = bare_screen_string.split("\n")
+ rows_filled = split_lines.count
+ $stdout.write "#{CSI}#{rows_filled};1H\n" # Moves console cursor to bottom
+ # of *content*, then one more.
end
- def last_keypress
- last_two_signals = user_signals.last(2)
- last_two_signals.last || last_two_signals.first
+ def tab_string
+ top_ro_preview = top_ro.value_string
+ tab = if session_is_current
+ "\e[7m #{tab_icon}: #{top_ro_preview} \e[00m"
+ else
+ " \e[7m#{tab_icon}:\e[00m #{top_ro_preview} "
+ end
+ tab
end
+ def subjects_of_target_ros
+ if special_command_targets.any?
+ return special_command_targets.map(&:current_subject)
+ end
+
+ current_ro.current_subject
+ end
+
private
+ def beep_if_user_had_to_wait(wait_start_time)
+ patient_keys = KEY_MAP.slice(*PATIENT_ACTIONS).values
+ user_has_waited_at_least_four_seconds =
+ Time.now - wait_start_time > 4 &&
+ !patient_keys.include?(last_keypress)
+ print "\a" if user_has_waited_at_least_four_seconds # (Audio 'beep')
+ end
+
+ def initiate_search
+ _screen_height, screen_width = $stdout.winsize
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
+ $stdout.print ' ' * screen_width
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
+ $stdout.print SEARCH_PROMPT
+ $stdout.write "#{CSI}1;#{SEARCH_PROMPT.ansiless_length + 1}H" # (Moves
+ # console cursor to just after the search prompt, before user types)
+ query = $stdin.gets.chomp
+ unless query.empty?
+ self.last_search = query
+ go_to_next_search_result
+ end
+ end
+
+ def move_cursor_up_action(action_count = nil)
+ action_count ||= !number_to_move.empty? ? number_to_move.to_i : 1
+ navigate_up_multiple(action_count)
+
+ self.number_to_move = ''
+ tree_view.slide_view_to_cursor
+ end
+
+ def move_cursor_down_action(action_count = nil)
+ action_count ||= !number_to_move.empty? ? number_to_move.to_i : 1
+ navigate_down_multiple(action_count)
+
+ self.number_to_move = ''
+ tree_view.slide_view_to_cursor
+ end
+
def clear_tracked_values
self.special_command_targets = []
self.last_search = nil
self.number_to_move = ''
end
@@ -268,10 +435,31 @@
bar = progress_bar.to_s
$stdout.write "#{CSI}#{screen_height};1H" # (Moves console cursor to bottom left corner)
print bar unless bar.tr(' ', '').empty?
end
+ def print_current_warning_messages
+ return if current_warning_messages.empty?
+
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
+ wing = ' ' * 3
+
+ self.current_warning_messages.reject! { |message| Time.now > message[:end_time] }
+ messages = current_warning_messages.map { |message| message[:text] }
+ print messages.map { |message| "\e[7m#{wing + message + wing}\e[00m" }.join("\n")
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
+ end
+
+ def print_session_tabs_bar_if_changed
+ seconds_in_tab = Time.now - session_view_start_time
+ if seconds_in_tab < 2
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
+ print session_manager.session_tabs_bar
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
+ end
+ end
+
def current_view_panel
view_panels[current_panel_type]
end
def tree_view
@@ -280,18 +468,10 @@
def lens_view
view_panels[:lens]
end
- def colorize(screen_string)
- dot = '•'
- cyan_dot = "\e[36m#{dot}\e[00m" # cyan then back to *default*
- screen_string.gsub!('•', cyan_dot)
-
- screen_string
- end
-
def display_active_searching_indicator
$stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
message = ' Searching... '
pad = SEARCH_PROMPT.length - message.length
wing = '-' * (pad / 2)
@@ -330,25 +510,20 @@
tree_view.recalculate_boundaries # Yes, necessary :)
lens_view.recalculate_boundaries # Yes, necessary :)
tree_view.current_view_coords = { y: 0, x: 0 }
tree_view.slide_view_to_cursor
else
- $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
- message = ' No Match Found '
- pad = SEARCH_PROMPT.length - message.length
- wing = '-' * (pad / 2)
-
- $stdout.write "\e[7m#{wing + message + wing}\e[00m"
- sleep 2
+ message = { text: 'No Match Found', end_time: Time.now + 2 }
+ self.current_warning_messages << message
end
end
def fetch_user_signal
previous_signal = user_signals.last
new_signal =
begin
- Timeout.timeout(0.1) { $stdin.getch }
+ Timeout.timeout(0.3) { $stdin.getch }
rescue Timeout::Error
nil
end
## Since many keys, including arrow keys, result in several signals being
@@ -379,20 +554,21 @@
Hexes.overwrite_screen(help_screen_string)
new_signal = fetch_user_signal
case new_signal
- when 'esc'
+ when nil
+ when KEY_MAP[:escape]
return true
- when '?'
+ when KEY_MAP[:control_screen]
current_help_screen_index += 1
- when 'q'
+ when KEY_MAP[:quit_session]
$stdout.write "#{CSI}#{screen_height};1H" # (Moves console cursor to
# bottom left corner). This helps 'q' not print the console prompt at
# the top of the screen, overlapping with the old display.
return false
- when "\u0003"
+ when KEY_MAP[:ctrl_c]
screen_height, _screen_width = $stdout.winsize
puts "\n" * screen_height
raise IRB::Abort, 'Ctrl+C Detected'
end
@@ -444,40 +620,71 @@
current_view_panel.recalculate_boundaries # This now happens at every screen
# draw to account for the user changing the screen size. Otherwise glitch.
current_view_panel.ensure_correct_view_coords
screen_string = current_view_panel.screen_string
- screen_string = colorize(screen_string) if Scryglass.config.dot_coloring
Hexes.overwrite_screen(screen_string)
$stdout.write "#{CSI}1;1H" # Moves terminal cursor to top left corner,
# mostly for consistency.
+ print_current_warning_messages
+ print_session_tabs_bar_if_changed
end
- def set_console_cursor_below_content
- bare_screen_string =
- current_view_panel.visible_header_string + "\n" +
- current_view_panel.visible_body_string
- split_lines = bare_screen_string.split("\n")
- rows_filled = split_lines.count
- $stdout.write "#{CSI}#{rows_filled};1H\n" # Moves console cursor to bottom
- # of *content*, then one more.
- end
-
- def visually_close_ui
+ def get_subject_name_from_user
_screen_height, screen_width = $stdout.winsize
- set_console_cursor_below_content
- puts '·' * screen_width, "\n"
- puts SESSION_CLOSED_MESSAGE
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
+ $stdout.print ' ' * screen_width
+ $stdout.write "#{CSI}1;1H" # (Moves console cursor to top left corner)
+ $stdout.print VARNAME_PROMPT
+ $stdout.write "#{CSI}1;#{VARNAME_PROMPT.ansiless_length + 1}H" # (Moves
+ # console cursor to just after the varname prompt, before user types)
+ $stdin.gets.chomp
end
- def subjects_of_target_ros
- if special_command_targets.any?
- return_targets = special_command_targets
- self.special_command_targets = []
- return return_targets.map(&:current_subject)
+ def name_subjects_of_target_ros
+ typed_name = get_subject_name_from_user
+ typed_name = typed_name.tr(' ', '')
+
+ if typed_name.empty?
+ message = { text: 'Instance Variable name cannot be blank',
+ end_time: Time.now + 2 }
+ self.current_warning_messages << message
+ print "\a" # (Audio 'beep')
+ return
end
- current_ro.current_subject
+ current_console_binding = session_manager.current_console_binding
+ preexisting_iv_names = current_console_binding
+ .eval('instance_variables') # Different than just `.instance_variables`
+ .map { |iv| iv.to_s.tr('@', '') }
+ all_method_names = preexisting_iv_names |
+ current_console_binding.methods |
+ current_console_binding.singleton_methods |
+ current_console_binding.private_methods
+ conflicting_method_name = all_method_names.find do |method_name|
+ pure_method_name = method_name.to_s.tr('=', '')
+ typed_name == pure_method_name
+ end
+
+ if conflicting_method_name
+ message = { text: 'Instance Variable name conflict',
+ end_time: Time.now + 2 }
+ self.current_warning_messages << message
+ print "\a" # (Audio 'beep')
+ return
+ end
+
+ set_iv_name_in_console =
+ "@#{typed_name} = " \
+ "$scry_session_manager.current_session.subjects_of_target_ros"
+ current_console_binding.eval(set_iv_name_in_console)
+ session_manager.current_binding_tracker.user_named_variables << "@#{typed_name}"
+
+ message = { text: "#{subjects_of_target_ros.class} assigned to: @#{typed_name}",
+ end_time: Time.now + 2 }
+ self.current_warning_messages << message
+
+ self.special_command_targets = []
end
def navigate_up_multiple(action_count)
task = Prog::Task.new(max_count: action_count)
progress_bar << task