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