scryglass 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scryglass
4
+ class Config
5
+ using ClipStringRefinement
6
+
7
+ attr_accessor :tab_length
8
+ attr_accessor :include_empty_associations,
9
+ :include_through_associations, :include_scoped_associations,
10
+ :show_association_types
11
+ attr_accessor :cursor_tracking
12
+ attr_accessor :lenses
13
+ attr_accessor :tree_view_key_string_clip_length,
14
+ :tree_view_value_string_clip_length
15
+ attr_accessor :dot_coloring
16
+
17
+ def initialize
18
+ ## Display
19
+ self.tab_length = 2 # You can make it 0, but please don't make it 0.
20
+ self.tree_view_key_string_clip_length = 200
21
+ self.tree_view_value_string_clip_length = 500
22
+ self.dot_coloring = true
23
+
24
+ ## UX
25
+ self.cursor_tracking = [:flexible_range, :dead_center][0] # One or the other
26
+ self.lenses = [ # Custom lenses can easily be added as name+lambda hashes! Or comment some out to turn them off.
27
+ { name: 'Pretty Print (`pp`)',
28
+ lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { pp o } } },
29
+ { name: 'Inspect (`.inspect`)',
30
+ lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { puts o.inspect } } },
31
+ { name: 'Yaml Print (`y`)',
32
+ lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { require 'yaml' ; y o } } }, # OR: `puts o.to_yaml`
33
+ { name: 'Puts (`puts`)',
34
+ lambda: ->(o) { Hexes.capture_io(char_limit: 20_000) { puts o } } },
35
+ # { name: 'Method Showcase',
36
+ # lambda: ->(o) { Scryglass::LensHelper.method_showcase_for(o) } },
37
+ ]
38
+
39
+ ## Building ActiveRecord association sub-rows:
40
+ self.include_empty_associations = true
41
+ self.include_through_associations = false
42
+ self.include_scoped_associations = false
43
+ self.show_association_types = true
44
+ end
45
+
46
+ def validate!
47
+ validate_boolean_attrs!
48
+ validate_positive_integer_attrs!
49
+ validate_cursor_tracking_options!
50
+ validate_lenses!
51
+ end
52
+
53
+ private
54
+
55
+ def validate_boolean_attrs!
56
+ bool_attrs = [
57
+ :include_empty_associations,
58
+ :include_through_associations,
59
+ :include_scoped_associations,
60
+ :dot_coloring,
61
+ ]
62
+ bool_attrs.each do |bool_attr|
63
+ value = send(bool_attr)
64
+ unless [true, false].include?(value)
65
+ raise ArgumentError, "#{bool_attr} must be true or false."
66
+ end
67
+ end
68
+ end
69
+
70
+ def validate_positive_integer_attrs!
71
+ positive_integer_attrs = [
72
+ :tab_length,
73
+ :tree_view_key_string_clip_length,
74
+ :tree_view_value_string_clip_length,
75
+ ]
76
+ positive_integer_attrs.each do |int_attr|
77
+ value = send(int_attr)
78
+ unless value.integer? && value.positive?
79
+ raise ArgumentError, "#{value} is not a positive integer."
80
+ end
81
+ end
82
+ end
83
+
84
+ def validate_cursor_tracking_options!
85
+ cursor_tracking_options = [:flexible_range, :dead_center]
86
+ unless cursor_tracking_options.include?(cursor_tracking)
87
+ raise ArgumentError, "#{cursor_tracking.inspect} not in " \
88
+ "[#{cursor_tracking_options.map(&:inspect).join(', ')}]."
89
+ end
90
+ end
91
+
92
+ def validate_lenses!
93
+ raise ArgumentError, 'lenses cannot be empty' unless lenses.any?
94
+
95
+ lenses.each do |lens|
96
+ unless lens.is_a?(Hash) && lens[:name].is_a?(String) && lens[:lambda].lambda?
97
+ raise ArgumentError, "Lens #{lens.inspect} must be a hash of the form:" \
98
+ '{ name: String, lambda: lambda }'
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scryglass
4
+ module LensHelper
5
+ def method_showcase_for(object)
6
+ method_list = object.methods - Object.methods
7
+ label_space = [method_list.map(&:length).max, 45].min
8
+ method_list.sort.map do |method_name|
9
+ label = method_name.to_s.ljust(label_space, ' ')
10
+ begin
11
+ method = object.method(method_name)
12
+ label + ' : ' +
13
+ method.source_location.to_a.join(':') + "\n" +
14
+ Hexes.capture_io { puts method.source }
15
+ rescue => e
16
+ label + ' : Error: ' +
17
+ e.message + "\n"
18
+ end
19
+ end.join("\n")
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scryglass
4
+ class LensPanel < Scryglass::ViewPanel
5
+ using ClipStringRefinement
6
+ using AnsilessStringRefinement
7
+ using ArrayFitToRefinement
8
+
9
+ private
10
+
11
+ def self.lenses
12
+ Scryglass.config.lenses
13
+ end
14
+
15
+ def uncut_body_string
16
+ current_lens = scry_session.current_lens
17
+ current_subject_type = scry_session.current_subject_type
18
+ current_subject = scry_session.current_ro.current_subject
19
+ ro_has_no_key = !scry_session.current_ro.key_value_pair?
20
+
21
+ return '' if ro_has_no_key && current_subject_type == :key
22
+
23
+ lens_id = current_lens % LensPanel.lenses.count
24
+ lens = LensPanel.lenses[lens_id]
25
+
26
+ scry_session.current_ro.lens_strings[current_subject_type][lens_id] ||=
27
+ begin
28
+ lens[:lambda].call(current_subject)
29
+ rescue => e
30
+ [e.message, *e.backtrace].join("\n")
31
+ end
32
+ end
33
+
34
+ def uncut_header_string
35
+ _screen_height, screen_width = $stdout.winsize
36
+ dotted_line = '·' * screen_width
37
+
38
+ [
39
+ current_ro_subheader,
40
+ dotted_line,
41
+ lens_param_subheader,
42
+ dotted_line
43
+ ].join("\n")
44
+ end
45
+
46
+ def visible_body_slice(uncut_body_string)
47
+ screen_height, screen_width = $stdout.winsize
48
+ non_header_view_size = screen_height - visible_header_string.split("\n").count
49
+
50
+ split_lines = uncut_body_string.split("\n")
51
+
52
+ ## Here we cut down the (rectangular) display array in both dimensions (into a smaller rectangle), as needed, to fit the view.
53
+ sliced_lines = split_lines.map do |string|
54
+ ansi_length = string.length - string.ansiless_length # Escape codes make `length` different from display length!
55
+ slice_length = screen_width + ansi_length
56
+ string[current_view_coords[:x], slice_length] || '' # If I don't want to
57
+ # opacify here, I need to account for nils when the view is fully
58
+ # beyond the shorter lines.
59
+ end
60
+ sliced_list = sliced_lines[current_view_coords[:y], non_header_view_size]
61
+
62
+ sliced_list.join("\n")
63
+ end
64
+
65
+ def recalculate_y_boundaries
66
+ self.y_boundaries = 0...(uncut_body_string.count("\n") + 1)
67
+ end
68
+
69
+ def recalculate_x_boundaries
70
+ _screen_height, screen_width = $stdout.winsize
71
+
72
+ split_lines = uncut_body_string.split("\n")
73
+ length_of_longest_line = split_lines.map(&:length).max || 0
74
+ max_line_length = [length_of_longest_line, screen_width].max
75
+
76
+ self.x_boundaries = 0...max_line_length
77
+ end
78
+
79
+ def current_ro_subheader
80
+ current_ro = scry_session.current_ro
81
+ user_input = scry_session.user_input
82
+
83
+ row_above_string =
84
+ current_ro.next_visible_ro_up.to_s if current_ro.next_visible_ro_up
85
+ row_below_string =
86
+ current_ro.next_visible_ro_down.to_s if current_ro.next_visible_ro_down
87
+
88
+ tree_preview_related_commands = ['A', 'B', 'C', 'D',
89
+ '@', '.', '(', '*', '|', '-']
90
+ ro_view_label =
91
+ if tree_preview_related_commands.include?(user_input)
92
+ "\e[7mVIEWING:\e[00m" # Color reversed
93
+ else
94
+ 'VIEWING:'
95
+ end
96
+
97
+ current_ro_window =
98
+ " #{row_above_string}\n" \
99
+ "#{ro_view_label} #{current_ro}\n" \
100
+ " #{row_below_string}"
101
+
102
+ current_ro_window
103
+ end
104
+
105
+ def lens_param_subheader
106
+ _screen_height, screen_width = $stdout.winsize
107
+
108
+ current_lens = scry_session.current_lens
109
+ current_subject_type = scry_session.current_subject_type
110
+ current_subject = scry_session.current_ro.current_subject
111
+ user_input = scry_session.user_input
112
+
113
+ lens_count = LensPanel.lenses.count
114
+ lens_id = current_lens % lens_count
115
+ lens = LensPanel.lenses[lens_id]
116
+
117
+ longest_lens_name_length = LensPanel.lenses.map do |lens|
118
+ lens[:name].length
119
+ end.max
120
+ lens_type_header_length = 9 + (lens_count.to_s.length * 2)
121
+ + longest_lens_name_length
122
+ subject_type_header = "SUBJECT: #{current_subject_type}".ljust(14, ' ')
123
+ subject_class_header = " CLASS: #{current_subject.class}"
124
+ lens_type_header = " LENS #{lens_id + 1}/#{lens_count}: #{lens[:name]}"
125
+ .ljust(lens_type_header_length, ' ')
126
+
127
+ fit_lens_header = [
128
+ subject_type_header, subject_class_header, lens_type_header
129
+ ].fit_to(screen_width)
130
+
131
+ if user_input == 'l'
132
+ fit_lens_header[4] = "\e[7m#{fit_lens_header[4]}" # Format to be ended by Hexes.opacify_screen_string() (using \e[00m)
133
+ elsif user_input == 'L'
134
+ fit_lens_header[0] = "\e[7m#{fit_lens_header[0]}\e[00m"
135
+ end
136
+
137
+ fit_lens_header.join('')
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scryglass
4
+ # A ro is essentially a complex wrapper for an object that deals with how it is
5
+ # nested and displayed relative to other ros/objects.
6
+ class Ro
7
+ using ClipStringRefinement
8
+
9
+ attr_accessor :key, :value, :val_type,
10
+ :key_string, :value_string, :lens_strings,
11
+ :key_value_relationship_indicator, :special_sub_ro_type,
12
+ :wrappers
13
+
14
+ attr_accessor :has_cursor, :expanded,
15
+ :parent_ro, :sub_ros,
16
+ :depth, :index, :scry_session
17
+
18
+ WRAPPER_TYPES = {
19
+ 'Hash' => '{}',
20
+ 'Array' => '[]',
21
+ 'ActiveRecord_Relation' => '<>',
22
+ 'ActiveRecord_Associations_CollectionProxy' => '‹›',
23
+ }.freeze
24
+
25
+ def initialize(scry_session:,
26
+ val:,
27
+ val_type:,
28
+ parent_ro:,
29
+ key:,
30
+ depth:,
31
+ key_value_relationship_indicator: false,
32
+ special_sub_ro_type: nil)
33
+ key_clip_length = Scryglass.config.tree_view_key_string_clip_length
34
+ value_clip_length = Scryglass.config.tree_view_value_string_clip_length
35
+
36
+ self.has_cursor = false
37
+ self.expanded = false
38
+
39
+ self.key_value_relationship_indicator = key_value_relationship_indicator
40
+
41
+ ## Open up ViewWrappers and grab their objects and their custom strings
42
+ if key.class == Scryglass::ViewWrapper
43
+ self.key_string = key.to_s.clip_at(key_clip_length)
44
+ self.key = key.model
45
+ else
46
+ self.key_string = key.inspect.clip_at(key_clip_length)
47
+ self.key = key
48
+ end
49
+ if val.class == Scryglass::ViewWrapper
50
+ self.value_string = val.to_s.clip_at(value_clip_length)
51
+ self.value = val.model
52
+ else
53
+ self.value_string = val.inspect.clip_at(value_clip_length)
54
+ self.value = val
55
+ end
56
+
57
+ self.sub_ros = []
58
+ self.parent_ro = parent_ro
59
+ self.val_type = val_type
60
+ self.special_sub_ro_type = special_sub_ro_type
61
+ self.depth = depth
62
+ self.wrappers = WRAPPER_TYPES[value.class.to_s.split('::').last] || '?¿'
63
+
64
+ self.lens_strings = { key: {}, value: {} }
65
+
66
+ self.index = scry_session.all_ros.count
67
+ scry_session.all_ros << self
68
+ self.scry_session = scry_session
69
+ end
70
+
71
+ def top_ro?
72
+ parent_ro.nil?
73
+ end
74
+
75
+ def to_s
76
+ value_indicator =
77
+ bucket? ? bucket_indicator : value_string
78
+
79
+ key_value_spacer =
80
+ key_value_pair? ? key_string + key_value_relationship_indicator : ''
81
+
82
+ special_sub_ro_expansion_indicator =
83
+ special_sub_ros.any? && !expanded ? '•' : ' '
84
+
85
+ left_fill_string + special_sub_ro_expansion_indicator +
86
+ key_value_spacer + value_indicator
87
+ end
88
+
89
+ def next_visible_ro_down
90
+ subsequent_ros = scry_session.all_ros[(index + 1)..-1]
91
+ subsequent_ros.find(&:visible?)
92
+ end
93
+
94
+ def next_visible_ro_up
95
+ preceding_ros = scry_session.all_ros[0...index]
96
+ preceding_ros.reverse.find(&:visible?)
97
+ end
98
+
99
+ def current_subject
100
+ send(scry_session.current_subject_type)
101
+ end
102
+
103
+ def iv_sub_ros
104
+ sub_ros.select { |sub_ro| sub_ro.special_sub_ro_type == :iv }
105
+ end
106
+
107
+ def ar_sub_ros
108
+ sub_ros.select { |sub_ro| sub_ro.special_sub_ro_type == :ar }
109
+ end
110
+
111
+ def enum_sub_ros
112
+ sub_ros.select { |sub_ro| sub_ro.special_sub_ro_type == :enum }
113
+ end
114
+
115
+ # (Used for recalculate_indeces after new Ros have been injected)
116
+ 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
129
+ end
130
+ end
131
+
132
+ def sibling_down
133
+ return nil if top_ro?
134
+
135
+ siblings = parent_ro.sub_ros
136
+ self_index = siblings.index(self)
137
+ return nil if self == siblings.last
138
+
139
+ siblings[self_index + 1]
140
+ end
141
+
142
+ ## This exists so that an easy *unordered array match* can occur elsewhere.
143
+ def <=>(other)
144
+ unless self.class == other.class
145
+ raise ArgumentError, "Comparison of #{self.class} with #{other.class}"
146
+ end
147
+
148
+ object_id <=> other.object_id
149
+ end
150
+
151
+
152
+ def visible?
153
+ return true if top_ro?
154
+
155
+ scanning_ro = parent_ro
156
+ until scanning_ro.top_ro? || !scanning_ro.expanded
157
+ scanning_ro = scanning_ro.parent_ro
158
+ end
159
+
160
+ scanning_ro.expanded
161
+ end
162
+
163
+ def bucket?
164
+ val_type == :bucket
165
+ end
166
+
167
+ def nugget?
168
+ val_type == :nugget
169
+ end
170
+
171
+ def key_value_pair?
172
+ !!key_value_relationship_indicator
173
+ end
174
+
175
+ private
176
+
177
+ def special_sub_ros
178
+ sub_ros.select(&:special_sub_ro_type)
179
+ end
180
+
181
+ def normal_sub_ros
182
+ sub_ros.reject(&:special_sub_ro_type)
183
+ end
184
+
185
+ def bucket_indicator
186
+ if expanded && normal_sub_ros.any?
187
+ wrappers[0]
188
+ elsif normal_sub_ros.any?
189
+ # Number of dots indicating order of magnitude for Enumerable's count:
190
+ # Turning this off (the consistent three dots is more like an ellipsis,
191
+ # communicating with a solid preexisting symbol), but keeping the idea here:
192
+ # sub_ros_order_of_magnitude = normal_sub_ros.count.to_s.length
193
+ # wrappers.dup.insert(1, '•' * sub_ros_order_of_magnitude)
194
+ wrappers.dup.insert(1, '•••')
195
+ else
196
+ wrappers
197
+ end
198
+ end
199
+
200
+ def left_fill_string
201
+ left_fill = if has_cursor
202
+ cursor_string
203
+ else
204
+ ' ' * cursor_length
205
+ end
206
+
207
+ if scry_session.special_command_targets.any? && scry_session.special_command_targets.map(&:index).include?(index)
208
+ left_fill[-2..-1] = '->'
209
+ end
210
+ left_fill
211
+ end
212
+
213
+ def cursor_length
214
+ tab_length = Scryglass.config.tab_length
215
+
216
+ consistent_margin = [4 - tab_length, 0].max
217
+
218
+ (tab_length * depth) + consistent_margin
219
+ end
220
+
221
+ def cursor_string
222
+ cursor = Scryglass::Session::CURSOR_CHARACTER * cursor_length
223
+
224
+ if nugget? && has_cursor && value.is_a?(Enumerable) &&
225
+ value.any? &&
226
+ enum_sub_ros.empty?
227
+ cursor[0] = '('
228
+ end
229
+
230
+ if value.instance_variables.any? && iv_sub_ros.empty?
231
+ cursor[1] = '@'
232
+ end
233
+
234
+ cursor
235
+ end
236
+ end
237
+ end