termplot 0.2.1 → 0.3.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +124 -54
  4. data/Rakefile +27 -14
  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 +16 -3
  15. data/lib/termplot/colors.rb +7 -0
  16. data/lib/termplot/commands.rb +27 -0
  17. data/lib/termplot/consumers.rb +12 -0
  18. data/lib/termplot/consumers/base_consumer.rb +132 -0
  19. data/lib/termplot/consumers/command_consumer.rb +14 -0
  20. data/lib/termplot/consumers/multi_source_consumer.rb +33 -0
  21. data/lib/termplot/consumers/single_source_consumer.rb +36 -0
  22. data/lib/termplot/consumers/stdin_consumer.rb +11 -0
  23. data/lib/termplot/cursors/buffered_console_cursor.rb +1 -1
  24. data/lib/termplot/cursors/virtual_cursor.rb +4 -0
  25. data/lib/termplot/dsl/panels.rb +80 -0
  26. data/lib/termplot/dsl/widgets.rb +128 -0
  27. data/lib/termplot/file_config.rb +37 -0
  28. data/lib/termplot/message_broker.rb +108 -0
  29. data/lib/termplot/options.rb +100 -20
  30. data/lib/termplot/positioned_widget.rb +8 -0
  31. data/lib/termplot/producer_options.rb +3 -0
  32. data/lib/termplot/producers.rb +3 -3
  33. data/lib/termplot/producers/base_producer.rb +12 -15
  34. data/lib/termplot/producers/command_producer.rb +25 -9
  35. data/lib/termplot/producers/stdin_producer.rb +1 -4
  36. data/lib/termplot/renderable.rb +35 -0
  37. data/lib/termplot/renderer.rb +16 -257
  38. data/lib/termplot/renderers.rb +6 -0
  39. data/lib/termplot/renderers/border_renderer.rb +48 -0
  40. data/lib/termplot/renderers/text_renderer.rb +73 -0
  41. data/lib/termplot/shell.rb +13 -9
  42. data/lib/termplot/utils/ansi_safe_string.rb +68 -0
  43. data/lib/termplot/version.rb +1 -1
  44. data/lib/termplot/widget_dsl.rb +130 -0
  45. data/lib/termplot/widgets.rb +8 -0
  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/window.rb +25 -5
  54. data/termplot.gemspec +1 -6
  55. metadata +36 -24
  56. data/doc/MSFT.png +0 -0
  57. data/doc/cpu.png +0 -0
  58. data/doc/demo.cast +0 -638
  59. data/lib/termplot/consumer.rb +0 -75
  60. data/lib/termplot/cursors/console_cursor.rb +0 -57
  61. data/lib/termplot/series.rb +0 -37
@@ -3,277 +3,36 @@
3
3
  require "termplot/window"
4
4
  require "termplot/character_map"
5
5
  require "termplot/colors"
6
+ require "termplot/renderable"
6
7
 
7
8
  module Termplot
8
9
  class Renderer
9
- attr_reader :cols, :rows
10
+ include Renderable
10
11
 
11
- def initialize(cols: 80, rows: 20, debug: false)
12
- # Default border size, right border allocation will change dynamically as
13
- # data comes in to account for the length of the numbers to be printed in
14
- # the axis ticks
15
- @border_size = default_border_size
16
- @cols = cols > min_cols ? cols : min_cols
17
- @rows = rows > min_rows ? rows : min_rows
18
- @window = Window.new(cols: @cols, rows: @rows)
19
- @decimals = 2
20
- @tick_spacing = 3
12
+ def initialize(cols: 80, rows: 20, debug: false, widgets:)
13
+ @window = Window.new(cols: cols, rows: rows)
14
+ @widgets = widgets
21
15
  @debug = debug
22
16
  @errors = []
23
17
  end
24
18
 
25
- def render(series)
19
+ def render_to_window
26
20
  window.clear
27
21
  errors.clear
28
22
 
29
- # Calculate width of right hand axis
30
- calculate_axis_size(series)
31
-
32
- # Points
33
- points = build_points(series)
34
- render_points(series, points)
35
- window.cursor.reset_position
36
-
37
- # Title bar
38
- render_title(series)
39
- window.cursor.reset_position
40
-
41
- # Borders
42
- render_borders
43
- window.cursor.reset_position
44
-
45
- # Draw axis
46
- ticks = build_ticks(points)
47
- render_axis(ticks)
48
-
49
- # Flush window to screen
50
- debug? ?
51
- window.flush_debug :
52
- window.flush
53
-
54
- if errors.any?
55
- window.print_errors(errors)
23
+ position = [0, 0]
24
+ widgets.each do |widget|
25
+ widget.render_to_window
26
+ window.blit(
27
+ widget.window,
28
+ widget.row,
29
+ widget.col
30
+ )
31
+ @errors.concat(widget.errors)
56
32
  end
57
33
  end
58
34
 
59
- def inner_width
60
- cols - border_size.left - border_size.right
61
- end
62
-
63
35
  private
64
- attr_reader :window, :border_size, :errors, :decimals, :tick_spacing
65
-
66
- def debug?
67
- @debug
68
- end
69
-
70
- def border_char_map
71
- CharacterMap::DEFAULT
72
- end
73
-
74
- def inner_height
75
- rows - border_size.top - border_size.bottom
76
- end
77
-
78
- # At minimum, 2 cols of inner_width for values
79
- def min_cols
80
- border_size.left + border_size.right + 2
81
- end
82
-
83
- # At minimum, 2 rows of inner_height for values
84
- def min_rows
85
- border_size.top + border_size.bottom + 2
86
- end
87
-
88
- Border = Struct.new(:top, :right, :bottom, :left)
89
- def default_border_size
90
- Border.new(2, 4, 1, 1)
91
- end
92
-
93
- # Axis size = length of the longest point value , formatted as a string to
94
- # @decimals decimal places, + 2 for some extra buffer + 1 for the border
95
- # itself.
96
- def calculate_axis_size(series)
97
- border_right = series.data.map { |n| n.round(decimals).to_s.length }.max
98
- border_right += 3
99
-
100
- # Clamp border_right at cols - 3 to prevent the renderer from crashing
101
- # with very large numbers
102
- if border_right > cols - 3
103
- errors.push(Colors.yellow "Warning: Axis tick values have been clipped, consider using more columns with -c")
104
- border_right = cols - 3
105
- end
106
-
107
- @border_size = Border.new(2, border_right, 1, 1)
108
- end
109
-
110
- Point = Struct.new(:x, :y, :value)
111
- def build_points(series)
112
- return [] if series.data.empty?
113
- points =
114
- series.data.last(inner_width).map.with_index do |p, x|
115
- # Map from series Y range to inner height
116
- y = map_value(p, [series.min, series.max], [0, inner_height - 1])
117
-
118
- # Invert Y value since pixel Y is inverse of cartesian Y
119
- y = border_size.top - 1 + inner_height - y.round
120
-
121
- # Add padding for border width
122
- Point.new(x + border_size.left, y, p.to_f)
123
- end
124
-
125
- points
126
- end
127
-
128
- Tick = Struct.new(:y, :label)
129
- def build_ticks(points)
130
- return [] if points.empty?
131
- max_point = points.max_by(&:value)
132
- min_point = points.min_by(&:value)
133
- point_y_range = points.max_by(&:y).y - points.min_by(&:y).y
134
- point_value_range = max_point.value - min_point.value
135
- ticks = []
136
- ticks.push Tick.new(max_point.y, format_label(max_point.value))
137
-
138
- # Distribute ticks between min and max, maintaining spacinig as much as
139
- # possible. tick_spacing is inclusive of the tick row itself.
140
- unless max_point.value == min_point.value &&
141
- (point_y_range - 2) > tick_spacing
142
- num_ticks = (point_y_range - 2) / tick_spacing
143
- num_ticks.times do |i|
144
- tick_y = max_point.y + (i + 1) * tick_spacing
145
- value = max_point.value - point_value_range * ((i + 1) * tick_spacing) / point_y_range
146
- ticks.push Tick.new(tick_y, format_label(value))
147
- end
148
- end
149
-
150
- ticks.push Tick.new(min_point.y, format_label(min_point.value))
151
- ticks
152
- end
153
-
154
- # Map value from one range to another
155
- def map_value(val, from_range, to_range)
156
- orig_range = [1, (from_range[1] - from_range[0]).abs].max
157
- new_range = [1, (to_range[1] - to_range[0]).abs].max
158
-
159
- ((val.to_f - from_range[0]) / orig_range) * new_range + to_range[0]
160
- end
161
-
162
- def render_points(series, points)
163
- # Render points
164
- points.each_with_index do |point, i|
165
- window.cursor.position = point.y * cols + point.x
166
- if series.line_style[:extended]
167
- prev_point = ((i - 1) >= 0) ? points[i-1] : nil
168
- render_connected_line(series, prev_point, point)
169
- else
170
- window.write(colored(series, series.line_style[:point]))
171
- end
172
- end
173
- end
174
-
175
- def render_connected_line(series, prev_point, point)
176
- if prev_point.nil? || (prev_point.y == point.y)
177
- window.write(colored(series, series.line_style[:horz_top]))
178
- elsif prev_point.y > point.y
179
- diff = prev_point.y - point.y
180
-
181
- window.write(colored(series, series.line_style[:top_left]))
182
- window.cursor.down
183
- window.cursor.back
184
-
185
- (diff - 1).times do
186
- window.write(colored(series, series.line_style[:vert_right]))
187
- window.cursor.down
188
- window.cursor.back
189
- end
190
-
191
- window.write(colored(series, series.line_style[:bot_right]))
192
- elsif prev_point.y < point.y
193
- diff = point.y - prev_point.y
194
-
195
- window.write(colored(series, series.line_style[:bot_left]))
196
- window.cursor.up
197
- window.cursor.back
198
-
199
- (diff - 1).times do
200
- window.write(colored(series, series.line_style[:vert_left]))
201
- window.cursor.up
202
- window.cursor.back
203
- end
204
-
205
- window.write(colored(series, series.line_style[:top_right]))
206
- end
207
- end
208
-
209
- def render_title(series)
210
- legend_marker = colored(series, series.line_style[:point])
211
- title = series.title
212
-
213
- legend_position = [1, (border_size.left + 1 + inner_width) / 2 - (title.length + 2) / 2].max
214
- if (title.length + 2 + legend_position) > cols
215
- errors.push(Colors.yellow "Warning: Title has been clipped, consider using more rows with -r")
216
- title = title[0..(cols - legend_position - 2)]
217
- end
218
-
219
- window.cursor.forward(legend_position)
220
- window.write(legend_marker)
221
- window.write(" ")
222
- title.chars.each do |char|
223
- window.write(char)
224
- end
225
- end
226
-
227
- def render_borders
228
- window.cursor.down(border_size.top - 1)
229
- window.cursor.forward(border_size.left - 1)
230
- window.write(border_char_map[:top_left])
231
- inner_width.times { window.write(border_char_map[:horz_top]) }
232
- window.write(border_char_map[:top_right])
233
- window.cursor.forward(border_size.right - 1)
234
-
235
- inner_height.times do |y|
236
- window.cursor.forward(border_size.left - 1)
237
- window.write(border_char_map[:vert_right])
238
- window.cursor.forward(inner_width)
239
- window.write(border_char_map[:vert_left])
240
- window.cursor.forward(border_size.right - 1)
241
- end
242
-
243
- # Bottom border
244
- # Jump to bottom left corner
245
- window.cursor.forward(border_size.left - 1)
246
- window.write(border_char_map[:bot_left])
247
- inner_width.times { window.write(border_char_map[:horz_top]) }
248
- window.write(border_char_map[:bot_right])
249
- end
250
-
251
- def render_axis(ticks)
252
- window.cursor.down(border_size.top - 1)
253
- window.cursor.forward(border_size.left + inner_width + 1)
254
-
255
- # Render ticks
256
- ticks.each do |tick|
257
- window.cursor.row = tick.y
258
- window.cursor.back
259
- window.write(border_char_map[:tick_right])
260
- tick.label.chars.each do |c|
261
- window.write(c)
262
- end
263
- window.cursor.back(label_chars)
264
- end
265
- end
266
-
267
- def format_label(num)
268
- ("%.2f" % num.round(decimals))[0..label_chars - 1].ljust(label_chars, " ")
269
- end
270
-
271
- def label_chars
272
- border_size.right - 2
273
- end
274
-
275
- def colored(series, text)
276
- Colors.send(series.color, text)
277
- end
36
+ attr_reader :cols, :rows, :widgets, :window, :errors
278
37
  end
279
38
  end
@@ -0,0 +1,6 @@
1
+ module Termplot
2
+ module Renderers
3
+ autoload :TextRenderer, "termplot/renderers/text_renderer"
4
+ autoload :BorderRenderer, "termplot/renderers/border_renderer"
5
+ end
6
+ end
@@ -0,0 +1,48 @@
1
+ require "termplot/character_map"
2
+
3
+ module Termplot
4
+ module Renderers
5
+ class BorderRenderer
6
+ attr_reader(
7
+ :window,
8
+ :border_char_map
9
+ )
10
+
11
+ def initialize(
12
+ bordered_window:,
13
+ border_char_map: CharacterMap::DEFAULT
14
+ )
15
+
16
+ @window = bordered_window
17
+ @border_char_map = border_char_map
18
+ end
19
+
20
+ def render
21
+ window.cursor.reset_position
22
+
23
+ # Top Border
24
+ window.cursor.down(window.border_size.top - 1)
25
+ window.cursor.forward(window.border_size.left - 1)
26
+ window.write(border_char_map[:top_left])
27
+ window.inner_width.times { window.write(border_char_map[:horz_top]) }
28
+ window.write(border_char_map[:top_right])
29
+ window.cursor.forward(window.border_size.right - 1)
30
+
31
+ # Left and right borders
32
+ window.inner_height.times do |y|
33
+ window.cursor.forward(window.border_size.left - 1)
34
+ window.write(border_char_map[:vert_right])
35
+ window.cursor.forward(window.inner_width)
36
+ window.write(border_char_map[:vert_left])
37
+ window.cursor.forward(window.border_size.right - 1)
38
+ end
39
+
40
+ # Bottom border
41
+ window.cursor.forward(window.border_size.left - 1)
42
+ window.write(border_char_map[:bot_left])
43
+ window.inner_width.times { window.write(border_char_map[:horz_top]) }
44
+ window.write(border_char_map[:bot_right])
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,73 @@
1
+ require "termplot/utils/ansi_safe_string"
2
+
3
+ module Termplot
4
+ module Renderers
5
+ class TextRenderer
6
+ attr_reader(
7
+ :window,
8
+ :text,
9
+ :row,
10
+ :errors,
11
+ :align
12
+ )
13
+
14
+ def initialize(
15
+ bordered_window:,
16
+ text:,
17
+ row:,
18
+ errors:,
19
+ align: :center
20
+ )
21
+
22
+ @window = bordered_window
23
+ @text = Termplot::Utils::AnsiSafeString.new(text)
24
+ @row = row
25
+ @errors = errors
26
+ @align = align
27
+ end
28
+
29
+ def render
30
+ window.cursor.row = row
31
+ window.cursor.beginning_of_line
32
+ window.cursor.forward(position)
33
+
34
+ clamped_text.each do |char|
35
+ window.write(char)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def clamped_text
42
+ if (text_length + position) > (window.cols - window.border_size.right - 1)
43
+ errors.push(
44
+ Colors.yellow("Warning: Text has been clipped, consider using more columns with -c")
45
+ )
46
+ text.slice(0, window.cols - position)
47
+ else
48
+ text
49
+ end
50
+ end
51
+
52
+ def position
53
+ @position ||= send("position_#{align}")
54
+ end
55
+
56
+ def position_center
57
+ [1, window.border_size.left + (window.inner_width - text_length + 1) / 2].max
58
+ end
59
+
60
+ def position_left
61
+ 0
62
+ end
63
+
64
+ def position_right
65
+ [1, window.border_size.left + window.inner_width - text_length].max
66
+ end
67
+
68
+ def text_length
69
+ @length ||= text.length
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,4 +1,4 @@
1
- require "termios"
1
+ require "io/console"
2
2
 
3
3
  module Termplot
4
4
  class Shell
@@ -7,22 +7,26 @@ module Termplot
7
7
 
8
8
  CURSOR_HIDE = "\e[?25l"
9
9
  CURSOR_SHOW = "\e[?25h"
10
- def init
10
+ CLEAR_SCREEN = "\e[2J"
11
+
12
+ def init(clear: false)
13
+ print CLEAR_SCREEN if clear
14
+
11
15
  # Disable echo on stdout tty, prevents printing chars if you type in
12
- # between rendering
13
- @termios_settings = Termios.tcgetattr($stdout)
14
- new_termios_settings = termios_settings.dup
15
- new_termios_settings.c_lflag &= ~(Termios::ECHO)
16
- Termios.tcsetattr($stdout, Termios::TCSAFLUSH, new_termios_settings)
16
+ STDOUT.echo = false
17
17
 
18
18
  print CURSOR_HIDE
19
19
  at_exit { reset }
20
20
  Signal.trap("INT") { exit(0) }
21
21
  end
22
22
 
23
+ # Leave a 1 char buffer on the right/bottom
24
+ def get_dimensions
25
+ STDOUT.winsize.map { |d| d - 1 }
26
+ end
27
+
23
28
  def reset
24
- # Reset stdout tty to original settings
25
- Termios.tcsetattr($stdout, Termios::TCSAFLUSH, termios_settings)
29
+ STDOUT.echo = true
26
30
 
27
31
  print CURSOR_SHOW
28
32
  end