class Scryglass::Session

Constants

CSI
CURSOR_CHARACTER
KEY_MAP
METHOD_NAME_PROMPT
PATIENT_ACTIONS
SEARCH_PROMPT
SUBJECT_TYPES
VARNAME_PROMPT

Attributes

all_ros[RW]
content_shape_changed[RW]
current_lens[RW]
current_panel_type[RW]
current_ro[RW]
current_subject_type[RW]
current_view_coords[RW]
current_warning_messages[RW]
number_to_move[RW]
previous_screen_dimensions[RW]
progress_bar[RW]
session_is_current[RW]
session_manager[RW]
session_view_start_time[RW]
signal_to_manager[RW]
special_command_targets[RW]
tab_icon[RW]
user_signals[RW]
view_panels[RW]

Public Class Methods

new(seed) click to toggle source
# File lib/scryglass/session.rb, line 94
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
  self.content_shape_changed = true
  self.previous_screen_dimensions = $stdout.winsize

  top_ro = roify(seed, parent_ro: nil, depth: 1)
  top_ro.has_cursor = true
  self.current_ro = top_ro

  expand!(top_ro)

  self.view_panels = {
    tree: Scryglass::TreePanel.new(scry_session: self),
    lens: Scryglass::LensPanel.new(scry_session: self),
  }
end

Public Instance Methods

last_keypress() click to toggle source
# File lib/scryglass/session.rb, line 128
def last_keypress
  last_two_signals = user_signals.last(2)
  last_two_signals.last || last_two_signals.first
end
run_scry_ui() click to toggle source
# File lib/scryglass/session.rb, line 133
def run_scry_ui
  redraw = true
  self.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 = []
  # when :playback
  #   if $scry_session_actions_performed.blank?
  #     raise 'Could not find recording of previous session\'s actions'
  #   end
  #   @input_stack = $scry_session_actions_performed.dup
  # end

  # 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 true
    draw_screen if redraw
    redraw = true

    ## On hold: Record/Playback Functionality:
    # case actions
    # when :record
    #   self.user_input = $stdin.getch
    #   $scry_session_actions_performed << user_input
    # when :playback
    #   if @input_stack.any? # (IV to be easily accessible for debugging)
    #     self.user_input = @input_stack.shift
    #     sleep 0.05
    #   else
    #     self.user_input = $stdin.getch
    #   end
    # else
    #   self.user_input = $stdin.getch
    # end

    new_signal = fetch_user_signal

    wait_start_time = Time.now

    case new_signal
    when nil
    when KEY_MAP[:escape]
      case current_panel_type
      when :lens
        self.current_panel_type = :tree
      when :tree
        clear_tracked_values
      end
    when KEY_MAP[:ctrl_c]
      set_console_cursor_below_content
      raise IRB::Abort, 'Ctrl+C Detected'
    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'
      # 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 KEY_MAP[:digit_3]
      self.number_to_move += '3'
      redraw = false
    when KEY_MAP[:digit_4]
      self.number_to_move += '4'
      redraw = false
    when KEY_MAP[:digit_5]
      self.number_to_move += '5'
      redraw = false
    when KEY_MAP[:digit_6]
      self.number_to_move += '6'
      redraw = false
    when KEY_MAP[:digit_7]
      self.number_to_move += '7'
      redraw = false
    when KEY_MAP[:digit_8]
      self.number_to_move += '8'
      redraw = false
    when KEY_MAP[:digit_9]
      self.number_to_move += '9'
      redraw = false
    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 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

    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 KEY_MAP[:homerow_close_bucket]
      collapse_targets

    when KEY_MAP[:toggle_view_panel]
      toggle_view_panel
    when KEY_MAP[:switch_lens]
      scroll_lens_type
    when KEY_MAP[:switch_subject_type]
      toggle_current_subject_type

    when KEY_MAP[:move_view_up]
      current_view_panel.move_view_up(5)
    when KEY_MAP[:move_view_down]
      current_view_panel.move_view_down(5)
    when KEY_MAP[:move_view_left]
      current_view_panel.move_view_left(5)
    when KEY_MAP[:move_view_right]
      current_view_panel.move_view_right(5)

    when KEY_MAP[:move_view_up_fast]
      current_view_panel.move_view_up(50)
    when KEY_MAP[:move_view_down_fast]
      current_view_panel.move_view_down(50)
    when KEY_MAP[:move_view_left_fast]
      current_view_panel.move_view_left(50)
    when KEY_MAP[:move_view_right_fast]
      current_view_panel.move_view_right(50)

    when KEY_MAP[:build_instance_variables]
      build_instance_variables_for_target_ros
      self.content_shape_changed = true
      tree_view.slide_view_to_cursor # Just a nice-to-have
    when KEY_MAP[:build_ar_relations]
      build_activerecord_relations_for_target_ros
      self.content_shape_changed = true
      tree_view.slide_view_to_cursor # Just a nice-to-have
    when KEY_MAP[:build_enum_children]
      build_enum_children_for_target_ros
      self.content_shape_changed = true
      tree_view.slide_view_to_cursor # Just a nice-to-have
    when KEY_MAP[:smart_open]
      smart_open_target_ros
      self.content_shape_changed = true
      tree_view.slide_view_to_cursor # Just a nice-to-have
    when KEY_MAP[:build_method_results]
      build_method_result_ros
      self.content_shape_changed = true
      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,
                      #   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 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 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 KEY_MAP[:start_search]
      initiate_search
    when KEY_MAP[:continue_search]
      # TODO: extract in separate commit
      if last_search
        go_to_next_search_result
      else
        message = { text: 'No Search has been entered', end_time: Time.now + 2 }
        self.current_warning_messages << message
      end
    when KEY_MAP[:start_new_session_from_target]
      self.signal_to_manager = :start_new_session_from_target
      return subjects_of_target_ros
    when KEY_MAP[:restart_session_from_target]
      self.signal_to_manager = :restart_session_from_target
      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
      return subjects_of_target_ros
    end

    beep_if_user_had_to_wait(wait_start_time)
  end
end
set_console_cursor_below_content(floor_the_cursor:) click to toggle source
# File lib/scryglass/session.rb, line 368
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
subjects_of_target_ros() click to toggle source
# File lib/scryglass/session.rb, line 394
def subjects_of_target_ros
  if special_command_targets.any?
    return special_command_targets.map(&:current_subject)
  end

  current_ro.current_subject
end
tab_string() click to toggle source
# File lib/scryglass/session.rb, line 384
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
top_ro() click to toggle source
# File lib/scryglass/session.rb, line 124
def top_ro
  all_ros.first
end

Private Instance Methods

beep_if_user_had_to_wait(wait_start_time) click to toggle source
# File lib/scryglass/session.rb, line 404
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
boot_screen() click to toggle source
# File lib/scryglass/session.rb, line 828
def boot_screen
  screen_height, screen_width = $stdout.winsize
  stars = (1..(screen_height * screen_width))
          .to_a
          .map { rand(100).zero? ? '.' : ' ' }
  stars.each_slice(screen_width).map { |set| set.join('') }.join("\n")
end
build_method_result_ros() click to toggle source
# File lib/scryglass/session.rb, line 685
def build_method_result_ros
  method_text = get_method_text_from_user

  if method_text.empty?
    message = { text: 'Call text cannot be blank',
                end_time: Time.now + 2 }
    self.current_warning_messages << message
    print "\a" # (Audio 'beep')
    return
  end

  if method_text[0] =~ /[a-z]|[A-Z]/
    message = { text: 'Call text must start with \'.\' or other symbol',
                end_time: Time.now + 3 }
    self.current_warning_messages << message
    print "\a" # (Audio 'beep')
    return
  end

  build_method_results_for_target_ros(method_text)

  self.special_command_targets = []
end
clear_tracked_values() click to toggle source
# File lib/scryglass/session.rb, line 447
def clear_tracked_values
  self.special_command_targets = []
  self.last_search = nil
  self.number_to_move = ''
end
collapse!(ro) click to toggle source
# File lib/scryglass/session.rb, line 779
def collapse!(ro)
  ro.expanded = false if ro.expanded
end
collapse_targets() click to toggle source
# File lib/scryglass/session.rb, line 601
def collapse_targets
  if special_command_targets.any?
    target_ros = special_command_targets.dup # dup because some commands
    #  create ros which are added to all_ros and then this process starts
    #  adding them to the list of things it tries to act on!
    target_ros.each { |target_ro| collapse!(target_ro) }
    self.special_command_targets = []
  elsif current_ro.expanded
    collapse!(current_ro)
  elsif current_ro.parent_ro
    collapse!(current_ro.parent_ro)
  end

  move_cursor_to(current_ro.parent_ro) until current_ro.visible?
  self.content_shape_changed = true
  tree_view.slide_view_to_cursor
end
current_view_panel() click to toggle source
# File lib/scryglass/session.rb, line 481
def current_view_panel
  view_panels[current_panel_type]
end
display_active_searching_indicator() click to toggle source
# File lib/scryglass/session.rb, line 493
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)

  $stdout.write "\e[7m#{wing + message + wing}\e[00m"
end
draw_screen() click to toggle source
# File lib/scryglass/session.rb, line 641
def draw_screen
  current_screen_dimensions = $stdout.winsize
  screen_size_changed = current_screen_dimensions != previous_screen_dimensions
  self.previous_screen_dimensions = current_screen_dimensions

  if content_shape_changed || screen_size_changed
    current_view_panel.recalculate_boundaries
    # ^This no longer happens at every screen draw, but only when
    #   determined necessary.
    self.content_shape_changed = false
  end

  current_view_panel.ensure_correct_view_coords
  screen_string = current_view_panel.screen_string

  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
expand!(ro) click to toggle source
# File lib/scryglass/session.rb, line 775
def expand!(ro)
  ro.expanded = true if ro.sub_ros.any?
end
expand_targets() click to toggle source
# File lib/scryglass/session.rb, line 619
def expand_targets
  if special_command_targets.any?
    target_ros = special_command_targets.dup # dup because some commands
    #  create ros which are added to all_ros and then this process starts
    #  adding them to the list of things it tries to act on!
    target_ros.each { |target_ro| expand!(target_ro) }
    self.special_command_targets = []
  else
    expand!(current_ro)
  end

  self.content_shape_changed = true
end
fetch_user_signal() click to toggle source
# File lib/scryglass/session.rb, line 539
def fetch_user_signal
  previous_signal = user_signals.last
  new_signal =
    begin
      Timeout.timeout(0.3) { $stdin.getch }
    rescue Timeout::Error
      nil
    end

  ## Since many keys, including arrow keys, result in several signals being
  ##   sent (e.g. DOWN: "\e" then "[" then "B" in RAPID succession), the
  ##   *pause* after a genuine escape key press (also "\e") is the only way
  ##   to distinguish it precisely.
  genuine_escape_key_press = new_signal.nil? && previous_signal == "\e"
  if genuine_escape_key_press
    new_signal = 'esc'
  end

  user_signals << new_signal unless new_signal.nil? && previous_signal.nil?

  new_signal
end
get_method_text_from_user() click to toggle source
# File lib/scryglass/session.rb, line 674
def get_method_text_from_user
  _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 METHOD_NAME_PROMPT
  $stdout.write "#{CSI}1;#{METHOD_NAME_PROMPT.ansiless_length + 1}H" # (Moves
  #   console cursor to just after the search prompt, before user types)
  $stdin.gets.chomp
end
get_subject_name_from_user() click to toggle source
# File lib/scryglass/session.rb, line 663
def get_subject_name_from_user
  _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 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
go_to_next_search_result() click to toggle source
# File lib/scryglass/session.rb, line 502
def go_to_next_search_result
  display_active_searching_indicator

  cut_point = current_ro.index
  search_set = ((cut_point + 1)...all_ros.count).to_a + (0...cut_point).to_a

  task = Prog::Task.new(max_count: search_set.count)
  progress_bar << task

  index_of_next_match = search_set.find do |index|
    scanned_ro = all_ros[index]
    task.tick
    print_progress_bar
    scanned_ro.key_string =~ /#{last_search}/ ||
      (scanned_ro.nugget? && scanned_ro.value_string =~ /#{last_search}/)
  end
  task.force_finish

  if index_of_next_match
    next_found_ro = all_ros[index_of_next_match]
    move_cursor_to(next_found_ro)

    scanning_ro = next_found_ro
    while scanning_ro.parent_ro
      expand!(scanning_ro.parent_ro)
      scanning_ro = scanning_ro.parent_ro
    end

    self.content_shape_changed = true # Needed here even if ros weren't expanded.
    tree_view.current_view_coords = { y: 0, x: 0 }
    tree_view.slide_view_to_cursor
  else
    message = { text: 'No Match Found', end_time: Time.now + 2 }
    self.current_warning_messages << message
  end
end
lens_view() click to toggle source
# File lib/scryglass/session.rb, line 489
def lens_view
  view_panels[:lens]
end
move_cursor_down_action(action_count = nil) click to toggle source
# File lib/scryglass/session.rb, line 437
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

  self.content_shape_changed = true if current_panel_type == :lens
end
move_cursor_to(new_ro) click to toggle source
# File lib/scryglass/session.rb, line 812
def move_cursor_to(new_ro)
  current_ro.has_cursor = false
  new_ro.has_cursor = true
  self.current_ro = new_ro
end
move_cursor_up_action(action_count = nil) click to toggle source
# File lib/scryglass/session.rb, line 427
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

  self.content_shape_changed = true if current_panel_type == :lens
end
name_subjects_of_target_ros() click to toggle source
# File lib/scryglass/session.rb, line 709
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_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 + 3 }
  self.current_warning_messages << message

  self.special_command_targets = []
end
navigate_down() click to toggle source
navigate_down_multiple(action_count) click to toggle source
navigate_up() click to toggle source
navigate_up_multiple(action_count) click to toggle source
print_current_warning_messages() click to toggle source
print_progress_bar() click to toggle source
print_session_tabs_bar_if_changed() click to toggle source
reset_the_view_or_cursor() click to toggle source
# File lib/scryglass/session.rb, line 633
def reset_the_view_or_cursor
  if current_view_panel.current_view_coords != { x: 0, y: 0 }
    current_view_panel.current_view_coords = { x: 0, y: 0 }
  elsif current_panel_type == :tree
    move_cursor_to(top_ro)
  end
end
run_help_screen_ui() click to toggle source
# File lib/scryglass/session.rb, line 562
def run_help_screen_ui
  screen_height, _screen_width = $stdout.winsize

  in_help_screen = true
  current_help_screen_index = 0
  help_screens = [Scryglass::HELP_SCREEN, Scryglass::HELP_SCREEN_ADVANCED]

  while in_help_screen
    current_help_screen = help_screens[current_help_screen_index]
    sliced_help_screen = Hexes.simple_screen_slice(current_help_screen)
    help_screen_string = Hexes.opacify_screen_string(sliced_help_screen)
    Hexes.overwrite_screen(help_screen_string)

    new_signal = fetch_user_signal

    case new_signal
    when nil
    when KEY_MAP[:escape]
      return true
    when KEY_MAP[:control_screen]
      current_help_screen_index += 1
    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 KEY_MAP[:ctrl_c]
      screen_height, _screen_width = $stdout.winsize
      puts "\n" * screen_height
      raise IRB::Abort, 'Ctrl+C Detected'
    end

    current_help_screen = help_screens[current_help_screen_index]
    unless current_help_screen
      return true
    end
  end
end
scroll_lens_type() click to toggle source
# File lib/scryglass/session.rb, line 807
def scroll_lens_type
  self.current_lens += 1
  self.content_shape_changed = true
end
toggle_current_subject_type() click to toggle source
# File lib/scryglass/session.rb, line 795
def toggle_current_subject_type
  self.current_subject_type =
    case current_subject_type
    when :value
      :key
    when :key
      :value
    end

  self.content_shape_changed = true
end
toggle_view_panel() click to toggle source
# File lib/scryglass/session.rb, line 783
def toggle_view_panel
  self.current_panel_type =
    case current_panel_type
    when :tree
      :lens
    when :lens
      :tree
    end

  self.content_shape_changed = true
end
tree_view() click to toggle source
# File lib/scryglass/session.rb, line 485
def tree_view
  view_panels[:tree]
end