termplot 0.1.0 → 0.3.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/README.md +134 -58
  4. data/Rakefile +28 -13
  5. data/doc/dash.png +0 -0
  6. data/doc/demo.png +0 -0
  7. data/doc/file.png +0 -0
  8. data/doc/memory.png +0 -0
  9. data/doc/ping.png +0 -0
  10. data/doc/sin.png +0 -0
  11. data/doc/tcp.png +0 -0
  12. data/examples/sample.rb +17 -0
  13. data/lib/termplot/character_map.rb +15 -4
  14. data/lib/termplot/cli.rb +14 -48
  15. data/lib/termplot/colors.rb +36 -29
  16. data/lib/termplot/commands.rb +27 -0
  17. data/lib/termplot/consumers/base_consumer.rb +132 -0
  18. data/lib/termplot/consumers/command_consumer.rb +14 -0
  19. data/lib/termplot/consumers/multi_source_consumer.rb +33 -0
  20. data/lib/termplot/consumers/single_source_consumer.rb +36 -0
  21. data/lib/termplot/consumers/stdin_consumer.rb +11 -0
  22. data/lib/termplot/consumers.rb +12 -0
  23. data/lib/termplot/{cursors/control_chars.rb → control_chars.rb} +0 -0
  24. data/lib/termplot/cursors/buffered_console_cursor.rb +51 -52
  25. data/lib/termplot/cursors/virtual_cursor.rb +64 -58
  26. data/lib/termplot/cursors.rb +7 -0
  27. data/lib/termplot/dsl/panels.rb +80 -0
  28. data/lib/termplot/dsl/widgets.rb +128 -0
  29. data/lib/termplot/file_config.rb +37 -0
  30. data/lib/termplot/message_broker.rb +111 -0
  31. data/lib/termplot/options.rb +211 -0
  32. data/lib/termplot/positioned_widget.rb +8 -0
  33. data/lib/termplot/producer_options.rb +3 -0
  34. data/lib/termplot/producers/base_producer.rb +32 -0
  35. data/lib/termplot/producers/command_producer.rb +42 -0
  36. data/lib/termplot/producers/stdin_producer.rb +11 -0
  37. data/lib/termplot/producers.rb +7 -0
  38. data/lib/termplot/renderable.rb +35 -0
  39. data/lib/termplot/renderer.rb +16 -257
  40. data/lib/termplot/renderers/border_renderer.rb +48 -0
  41. data/lib/termplot/renderers/text_renderer.rb +73 -0
  42. data/lib/termplot/renderers.rb +6 -0
  43. data/lib/termplot/shell.rb +13 -9
  44. data/lib/termplot/utils/ansi_safe_string.rb +68 -0
  45. data/lib/termplot/version.rb +1 -1
  46. data/lib/termplot/widgets/base_widget.rb +79 -0
  47. data/lib/termplot/widgets/border.rb +6 -0
  48. data/lib/termplot/widgets/dataset.rb +50 -0
  49. data/lib/termplot/widgets/histogram_widget.rb +196 -0
  50. data/lib/termplot/widgets/statistics.rb +21 -0
  51. data/lib/termplot/widgets/statistics_widget.rb +104 -0
  52. data/lib/termplot/widgets/time_series_widget.rb +248 -0
  53. data/lib/termplot/widgets.rb +8 -0
  54. data/lib/termplot/window.rb +29 -9
  55. data/termplot.gemspec +1 -6
  56. metadata +46 -30
  57. data/doc/cpu.png +0 -0
  58. data/doc/demo.cast +0 -638
  59. data/doc/demo.gif +0 -0
  60. data/lib/termplot/consumer.rb +0 -71
  61. data/lib/termplot/cursors/console_cursor.rb +0 -56
  62. data/lib/termplot/series.rb +0 -37
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "termplot/window"
4
+ require "termplot/renderable"
5
+ require "termplot/window"
6
+ require "termplot/character_map"
7
+ require "termplot/renderers"
8
+ require "termplot/colors"
9
+
10
+ module Termplot
11
+ module Widgets
12
+ class HistogramWidget < BaseWidget
13
+ DEFAULT_COLOR = "green"
14
+ attr_reader :color
15
+
16
+ def post_initialize(opts)
17
+ @color = opts[:color] || DEFAULT_COLOR
18
+ end
19
+
20
+ def render_to_window
21
+ errors.clear
22
+ window.clear
23
+ window.cursor.reset_position
24
+
25
+ bins = calculate_bins
26
+ bins = bin_data(bins)
27
+ bins = calculate_bin_coordinates(bins)
28
+ calculate_axis_size(bins)
29
+ render_bins(bins)
30
+ window.cursor.reset_position
31
+
32
+ # Title bar
33
+ Termplot::Renderers::TextRenderer.new(
34
+ bordered_window: bordered_window,
35
+ text: title_text,
36
+ row: 0,
37
+ errors: errors
38
+ ).render
39
+
40
+ window.cursor.reset_position
41
+
42
+ # Borders
43
+ Termplot::Renderers::BorderRenderer.new(
44
+ bordered_window: bordered_window
45
+ ).render
46
+
47
+ window.cursor.reset_position
48
+
49
+ # Ticks
50
+ render_ticks(bins)
51
+ window.cursor.reset_position
52
+ end
53
+
54
+ private
55
+
56
+ def default_border_size
57
+ Border.new(2, 1, 1, 4)
58
+ end
59
+
60
+ def calculate_axis_size(bins)
61
+ return if bins.empty?
62
+ border_left = bins.map { |bin| format_tick_label(bin.midpoint).length }.max
63
+ border_left += 2
64
+
65
+ # Clamp border_left to prevent the renderer from crashing
66
+ # with very large numbers
67
+ if border_left > cols - 5
68
+ errors.push(Colors.yellow("Warning: Axis tick values have been clipped, consider using more columns with -c"))
69
+ border_left = cols - 5
70
+ end
71
+
72
+ @bordered_window.border_size = Border.new(2, 1, 1, border_left)
73
+ end
74
+
75
+ def min_cols
76
+ default_border_size.left + default_border_size.right + 5
77
+ end
78
+
79
+ def num_bins
80
+ bordered_window.inner_height
81
+ end
82
+
83
+ def min_rows
84
+ default_border_size.top + default_border_size.bottom + 1
85
+ end
86
+
87
+ def title_text
88
+ bin_char + " " + title
89
+ end
90
+
91
+ def render_bins(positioned_bins)
92
+ positioned_bins.each do |bin|
93
+ window.cursor.beginning_of_line
94
+ window.cursor.row = bin.y + bordered_window.border_size.top
95
+ window.cursor.forward(bordered_window.border_size.left)
96
+ bin.x.times { window.write(bin_char) }
97
+ window.write(" ")
98
+
99
+ bin.count.to_s.chars.each do |char|
100
+ window.write(char)
101
+ end
102
+ end
103
+ end
104
+
105
+ def render_ticks(positioned_bins)
106
+ positioned_bins.each do |bin|
107
+ window.cursor.row = bin.y + bordered_window.border_size.top
108
+ window.cursor.beginning_of_line
109
+
110
+ format_tick_label(bin.midpoint).rjust(bordered_window.border_size.left - 2, " ").chars.first(bordered_window.border_size.left - 2).each do |c|
111
+ window.write(c)
112
+ end
113
+ window.write(" ")
114
+ window.write(border_char_map[:tick_right])
115
+ end
116
+ end
117
+
118
+ def border_char_map
119
+ CharacterMap::DEFAULT
120
+ end
121
+
122
+ def format_tick_label(value)
123
+ "%.#{decimals}f" % value.round(decimals)
124
+ end
125
+
126
+ PositionedBin = Struct.new(:bin, :x, :y) do
127
+ extend(Forwardable)
128
+ def_delegators(:bin, :count, :min, :max, :midpoint)
129
+ end
130
+
131
+ def calculate_bin_coordinates(bins)
132
+ return [] unless bins.any?
133
+ max_count = bins.max_by { |bin| bin.count }&.count
134
+
135
+ bins.map.with_index do |bin, i|
136
+ row = i
137
+ # Save some chars for count
138
+ col = ((bin.count.to_f / max_count) * (bordered_window.inner_width - 4)).floor
139
+ PositionedBin.new(bin, col, row)
140
+ end
141
+ end
142
+
143
+ def bin_data(bins)
144
+ return [] unless bins.any?
145
+
146
+ dataset.each do |value|
147
+ bin = bins.find { |b| b.min <= value && b.max > value }
148
+ bin.count += 1 unless bin.nil?
149
+ end
150
+
151
+ bins
152
+ end
153
+
154
+ Bin = Struct.new(:min, :max, :count) do
155
+ def size
156
+ max - min
157
+ end
158
+
159
+ def midpoint
160
+ (max + min) / 2
161
+ end
162
+ end
163
+
164
+ def calculate_bins
165
+ return [] if dataset.empty?
166
+
167
+ min = dataset.min
168
+ max = dataset.max
169
+ bin_size = dataset.range.to_f / num_bins.to_f
170
+
171
+ if bin_size.zero?
172
+ min -= 1
173
+ max += 1
174
+ bin_size = 1
175
+ end
176
+
177
+ bins = []
178
+ while min < max && bins.length < num_bins
179
+ bins.push(Bin.new(min, min + bin_size, 0))
180
+ min += bin_size
181
+ end
182
+
183
+ # Correct for floating point errors on max bin
184
+ if bins.any?
185
+ bins.last.max = max if bins.last.max < max
186
+ end
187
+
188
+ bins
189
+ end
190
+
191
+ def bin_char
192
+ Colors.send(color, "▇")
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,21 @@
1
+ module Termplot
2
+ module Widgets
3
+ module Statistics
4
+ def count
5
+ data.count
6
+ end
7
+
8
+ def mean
9
+ return 0 if data.empty?
10
+ data.sum(0.0) / count
11
+ end
12
+
13
+ def standard_deviation
14
+ return 0 if data.empty?
15
+ data_mean = mean
16
+ variance = data.map { |x| (data_mean - x) ** 2 }.sum / count
17
+ Math.sqrt(variance)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "termplot/window"
4
+ require "termplot/renderable"
5
+ require "termplot/window"
6
+ require "termplot/character_map"
7
+ require "termplot/renderers"
8
+
9
+ module Termplot
10
+ module Widgets
11
+ class StatisticsWidget < BaseWidget
12
+ def render_to_window
13
+ errors.clear
14
+ window.clear
15
+ window.cursor.reset_position
16
+
17
+ render_statistics
18
+ window.cursor.reset_position
19
+
20
+ # Title bar
21
+ Termplot::Renderers::TextRenderer.new(
22
+ bordered_window: bordered_window,
23
+ text: title,
24
+ row: 0,
25
+ align: :center,
26
+ errors: errors
27
+ ).render
28
+
29
+ window.cursor.reset_position
30
+
31
+ # Borders
32
+ Termplot::Renderers::BorderRenderer.new(
33
+ bordered_window: bordered_window
34
+ ).render
35
+
36
+ window.cursor.reset_position
37
+ end
38
+
39
+ private
40
+ def default_border_size
41
+ Border.new(2, 1, 1, 1)
42
+ end
43
+
44
+ def min_cols
45
+ 20
46
+ end
47
+
48
+ def min_rows
49
+ 5 + default_border_size.top + default_border_size.bottom
50
+ end
51
+
52
+ def render_statistics
53
+ titles, values = formatted_stats
54
+
55
+ window.cursor.down(bordered_window.border_size.top)
56
+ window.cursor.beginning_of_line
57
+ window.cursor.forward(bordered_window.border_size.left)
58
+
59
+ title_color = "blue"
60
+ value_color = "green"
61
+
62
+ justified_stats = titles.zip(values).map do |(title, value)|
63
+ field_size = [title.size, value.size].max
64
+ title = Colors.send(title_color, title.ljust(field_size, " "))
65
+ value = Colors.send(value_color, value.ljust(field_size, " "))
66
+ [title, value]
67
+ end
68
+
69
+ col_separator = " #{border_char_map[:vert_right]} "
70
+ stats_table = justified_stats.transpose.map { |row| row.join(col_separator) }
71
+
72
+ start_row = bordered_window.inner_height > 2 ? bordered_window.border_size.top - 1 + bordered_window.inner_height / 2 : bordered_window.border_size.top
73
+
74
+ stats_table.each_with_index do |row, index|
75
+ Termplot::Renderers::TextRenderer.new(
76
+ bordered_window: bordered_window,
77
+ text: row,
78
+ row: start_row + index,
79
+ errors: errors,
80
+ align: :center
81
+ ).render
82
+ end
83
+ end
84
+
85
+ def formatted_stats
86
+ titles = %w[Samples Min Max Mean Stdev]
87
+
88
+ values = [:count, :min, :max, :mean, :standard_deviation].map do |stat|
89
+ format_number(dataset.send(stat))
90
+ end
91
+
92
+ [titles, values]
93
+ end
94
+
95
+ def format_number(n)
96
+ "%.#{decimals}f" % n.round(decimals)
97
+ end
98
+
99
+ def border_char_map
100
+ CharacterMap::DEFAULT
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "termplot/window"
4
+ require "termplot/character_map"
5
+ require "termplot/colors"
6
+ require "termplot/renderable"
7
+ require "termplot/renderers"
8
+
9
+ module Termplot
10
+ module Widgets
11
+ class TimeSeriesWidget < BaseWidget
12
+ DEFAULT_COLOR = "yellow"
13
+ DEFAULT_LINE_STYLE = "line"
14
+
15
+ attr_reader :color, :line_style, :tick_spacing
16
+
17
+ def post_initialize(opts)
18
+ @color = Termplot::Colors.fetch(opts[:color], DEFAULT_COLOR)
19
+ @line_style = Termplot::CharacterMap::LINE_STYLES.fetch(
20
+ opts[:line_style],
21
+ Termplot::CharacterMap::LINE_STYLES[DEFAULT_LINE_STYLE]
22
+ )
23
+
24
+ @tick_spacing = 3
25
+ end
26
+
27
+ def render_to_window
28
+ errors.clear
29
+ window.clear
30
+
31
+ # Calculate width of right hand axis
32
+ calculate_axis_size
33
+
34
+ # Points
35
+ points = build_points
36
+ render_points(points)
37
+ window.cursor.reset_position
38
+
39
+ # Title bar
40
+ Termplot::Renderers::TextRenderer.new(
41
+ bordered_window: bordered_window,
42
+ text: title_text,
43
+ row: 0,
44
+ errors: errors
45
+ ).render
46
+ window.cursor.reset_position
47
+
48
+ # Borders
49
+ Termplot::Renderers::BorderRenderer.new(
50
+ bordered_window: bordered_window
51
+ ).render
52
+
53
+ window.cursor.reset_position
54
+
55
+ # Draw axis
56
+ ticks = build_ticks(points)
57
+ render_axis(ticks)
58
+ end
59
+
60
+ private
61
+ def max_count
62
+ bordered_window.inner_width
63
+ end
64
+
65
+ # Axis size = length of the longest point value , formatted as a string to
66
+ # @decimals decimal places, + 2 for some extra buffer + 1 for the border
67
+ # itself.
68
+ def calculate_axis_size
69
+ return if dataset.empty?
70
+ border_right = dataset.map { |n| n.round(decimals).to_s.length }.max
71
+ border_right += 3
72
+
73
+ # Clamp border_right at cols - 3 to prevent the renderer from crashing
74
+ # with very large numbers
75
+ if border_right > cols - 3
76
+ errors.push(Colors.yellow("Warning: Axis tick values have been clipped, consider using more columns with -c"))
77
+ border_right = cols - 3
78
+ end
79
+
80
+ @bordered_window.border_size = Border.new(2, border_right, 1, 1)
81
+ end
82
+
83
+ def border_char_map
84
+ CharacterMap::DEFAULT
85
+ end
86
+
87
+ def default_border_size
88
+ Border.new(2, 4, 1, 1)
89
+ end
90
+
91
+ # At minimum, 2 cols of inner_width for values
92
+ def min_cols
93
+ default_border_size.left + default_border_size.right + 2
94
+ end
95
+
96
+ # At minimum, 2 rows of inner_height for values
97
+ def min_rows
98
+ default_border_size.top + default_border_size.bottom + 2
99
+ end
100
+
101
+ Point = Struct.new(:x, :y, :value)
102
+ def build_points
103
+ return [] if dataset.empty?
104
+
105
+ dataset.map.with_index do |p, x|
106
+ # Map from series Y range to inner height
107
+ y = map_value(p, [dataset.min, dataset.max], [0, bordered_window.inner_height - 1])
108
+
109
+ # Invert Y value since pixel Y is inverse of cartesian Y
110
+ y = bordered_window.border_size.top - 1 + bordered_window.inner_height - y.round
111
+
112
+ # Add padding for border width
113
+ Point.new(x + bordered_window.border_size.left, y, p.to_f)
114
+ end
115
+ end
116
+
117
+ def render_points(points)
118
+ # Render points
119
+ points.each_with_index do |point, i|
120
+ window.cursor.position = point.y * cols + point.x
121
+
122
+ if line_style[:extended]
123
+ prev_point = ((i - 1) >= 0) ? points[i - 1] : nil
124
+ render_connected_line(prev_point, point)
125
+ elsif line_style[:filled]
126
+ render_filled_point(point)
127
+ else
128
+ window.write(colored(line_style[:point]))
129
+ end
130
+ end
131
+ end
132
+
133
+ def render_connected_line(prev_point, point)
134
+ if prev_point.nil? || (prev_point.y == point.y)
135
+ window.write(colored(line_style[:horz_top]))
136
+ elsif prev_point.y > point.y
137
+ diff = prev_point.y - point.y
138
+
139
+ window.write(colored(line_style[:top_left]))
140
+ window.cursor.down
141
+ window.cursor.back
142
+
143
+ (diff - 1).times do
144
+ window.write(colored(line_style[:vert_right]))
145
+ window.cursor.down
146
+ window.cursor.back
147
+ end
148
+
149
+ window.write(colored(line_style[:bot_right]))
150
+ elsif prev_point.y < point.y
151
+ diff = point.y - prev_point.y
152
+
153
+ window.write(colored(line_style[:bot_left]))
154
+ window.cursor.up
155
+ window.cursor.back
156
+
157
+ (diff - 1).times do
158
+ window.write(colored(line_style[:vert_left]))
159
+ window.cursor.up
160
+ window.cursor.back
161
+ end
162
+
163
+ window.write(colored(line_style[:top_right]))
164
+ end
165
+ end
166
+
167
+ def render_filled_point(point)
168
+ diff = (bordered_window.inner_height + bordered_window.border_size.bottom) - point.y
169
+ diff.times { window.cursor.down }
170
+
171
+ diff.times do
172
+ window.write(Colors.send("#{color}_bg", colored(line_style[:point])))
173
+ window.cursor.up
174
+ window.cursor.back
175
+ end
176
+
177
+ window.write(colored(line_style[:point]))
178
+ end
179
+
180
+ Tick = Struct.new(:y, :label)
181
+ def build_ticks(points)
182
+ return [] if points.empty?
183
+ max_point = points.max_by(&:value)
184
+ min_point = points.min_by(&:value)
185
+ point_y_range = points.max_by(&:y).y - points.min_by(&:y).y
186
+ ticks = []
187
+ ticks.push(Tick.new(max_point.y, format_label(max_point.value)))
188
+
189
+ # Distribute ticks between min and max, maintaining spacinig as much as
190
+ # possible. tick_spacing is inclusive of the tick row itself.
191
+ if max_point.value != min_point.value && (point_y_range - 2) > tick_spacing
192
+ num_ticks = (point_y_range - 2) / tick_spacing
193
+
194
+ num_ticks.times do |i|
195
+ tick_y = max_point.y + (i + 1) * tick_spacing
196
+ value = max_point.value - dataset.range * ((i + 1) * tick_spacing) / point_y_range
197
+ ticks.push(Tick.new(tick_y, format_label(value)))
198
+ end
199
+ end
200
+
201
+ ticks.push(Tick.new(min_point.y, format_label(min_point.value)))
202
+ ticks
203
+ end
204
+
205
+ # Map value from one range to another
206
+ def map_value(val, from_range, to_range)
207
+ orig_range = [1, (from_range[1] - from_range[0]).abs].max
208
+ new_range = [1, (to_range[1] - to_range[0]).abs].max
209
+
210
+ ((val.to_f - from_range[0]) / orig_range) * new_range + to_range[0]
211
+ end
212
+
213
+ def title_text
214
+ colored(line_style[:point]) + " " + title
215
+ end
216
+
217
+ def render_axis(ticks)
218
+ window.cursor.down(bordered_window.border_size.top - 1)
219
+ window.cursor.forward(bordered_window.border_size.left + bordered_window.inner_width + 1)
220
+
221
+ # Render ticks
222
+ ticks.each do |tick|
223
+ window.cursor.row = tick.y
224
+ window.cursor.back
225
+ window.write(border_char_map[:tick_right])
226
+
227
+ tick.label.chars.each do |c|
228
+ window.write(c)
229
+ end
230
+
231
+ window.cursor.back(label_chars)
232
+ end
233
+ end
234
+
235
+ def format_label(num)
236
+ ("%.#{decimals}f" % num.round(decimals))[0..label_chars - 1].ljust(label_chars, " ")
237
+ end
238
+
239
+ def label_chars
240
+ bordered_window.border_size.right - 2
241
+ end
242
+
243
+ def colored(text)
244
+ Colors.send(color, text)
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,8 @@
1
+ module Termplot
2
+ module Widgets
3
+ autoload :BaseWidget, "termplot/widgets/base_widget"
4
+ autoload :TimeSeriesWidget, "termplot/widgets/time_series_widget"
5
+ autoload :StatisticsWidget, "termplot/widgets/statistics_widget"
6
+ autoload :HistogramWidget, "termplot/widgets/histogram_widget"
7
+ end
8
+ end
@@ -1,5 +1,5 @@
1
- require "termplot/cursors/virtual_cursor"
2
- require "termplot/cursors/buffered_console_cursor"
1
+ require "termplot/control_chars"
2
+ require "termplot/cursors"
3
3
 
4
4
  module Termplot
5
5
  class Window
@@ -11,14 +11,14 @@ module Termplot
11
11
  end
12
12
 
13
13
  def cursor
14
- @cursor ||= VirtualCursor.new(self)
14
+ @cursor ||= Termplot::Cursors::VirtualCursor.new(self)
15
15
  end
16
16
 
17
17
  def console_cursor
18
18
  # Console buffer has an extra rows - 1 to account for new line characters
19
19
  # between rows
20
20
  @console_cursor ||=
21
- BufferedConsoleCursor.new(self, Array.new(cols * rows + rows - 1))
21
+ Termplot::Cursors::BufferedConsoleCursor.new(self, Array.new(cols * rows + rows - 1))
22
22
  end
23
23
 
24
24
  def size
@@ -35,6 +35,7 @@ module Termplot
35
35
  size.times { write CharacterMap::DEFAULT[:empty] }
36
36
  end
37
37
 
38
+ # Flush rendered window to a string
38
39
  def flush
39
40
  console_cursor.clear_buffer
40
41
  console_cursor.reset_position
@@ -47,16 +48,17 @@ module Termplot
47
48
  console_cursor.flush
48
49
  end
49
50
 
50
- def flush_debug(str = "Window")
51
- padding = "-" * 10
52
- puts "\n#{padding} #{str} #{padding}\n"
51
+ # Flush to 2d array rather than string
52
+ def flush_debug
53
+ debug_arr = []
53
54
  buffer.each_slice(cols).with_index do |line, y|
54
55
  render_line = line.each_with_index.map do |c, x|
55
56
  y * cols + x == cursor.position ? "𝥺" : c
56
57
  end
57
- print render_line
58
- puts
58
+ debug_arr << render_line
59
+ debug_arr << "\n"
59
60
  end
61
+ debug_arr
60
62
  end
61
63
 
62
64
  # TODO: Refine later and include errors properly in the window
@@ -65,5 +67,23 @@ module Termplot
65
67
  print Termplot::ControlChars::NEWLINE
66
68
  errors.length.times { print Termplot::ControlChars::UP }
67
69
  end
70
+
71
+ def blit(other, start_row, start_col)
72
+ cursor.position = start_row * cols + start_col
73
+
74
+ other.each_row do |row|
75
+ row.each do |char|
76
+ write(char)
77
+ end
78
+ cursor.down unless cursor.beginning_of_line?
79
+ cursor.col = start_col
80
+ end
81
+ end
82
+
83
+ def each_row
84
+ buffer.each_slice(cols) do |row|
85
+ yield row
86
+ end
87
+ end
68
88
  end
69
89
  end
data/termplot.gemspec CHANGED
@@ -10,9 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = %q{Plot time series charts in your terminal}
11
11
  spec.homepage = "https://github.com/Martin-Nyaga/termplot"
12
12
  spec.license = "MIT"
13
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
-
15
- # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
16
14
 
17
15
  spec.metadata["homepage_uri"] = spec.homepage
18
16
  spec.metadata["source_code_uri"] = "https://github.com/Martin-Nyaga/termplot"
@@ -26,7 +24,4 @@ Gem::Specification.new do |spec|
26
24
  spec.bindir = "bin"
27
25
  spec.executables = "termplot"
28
26
  spec.require_paths = ["lib"]
29
-
30
- # Dependencies
31
- spec.add_runtime_dependency "ruby-termios", "~> 1.0"
32
27
  end