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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +124 -54
- data/Rakefile +27 -14
- data/doc/dash.png +0 -0
- data/doc/demo.png +0 -0
- data/doc/file.png +0 -0
- data/doc/memory.png +0 -0
- data/doc/ping.png +0 -0
- data/doc/sin.png +0 -0
- data/doc/tcp.png +0 -0
- data/examples/sample.rb +17 -0
- data/lib/termplot/character_map.rb +15 -4
- data/lib/termplot/cli.rb +16 -3
- data/lib/termplot/colors.rb +7 -0
- data/lib/termplot/commands.rb +27 -0
- data/lib/termplot/consumers.rb +12 -0
- data/lib/termplot/consumers/base_consumer.rb +132 -0
- data/lib/termplot/consumers/command_consumer.rb +14 -0
- data/lib/termplot/consumers/multi_source_consumer.rb +33 -0
- data/lib/termplot/consumers/single_source_consumer.rb +36 -0
- data/lib/termplot/consumers/stdin_consumer.rb +11 -0
- data/lib/termplot/cursors/buffered_console_cursor.rb +1 -1
- data/lib/termplot/cursors/virtual_cursor.rb +4 -0
- data/lib/termplot/dsl/panels.rb +80 -0
- data/lib/termplot/dsl/widgets.rb +128 -0
- data/lib/termplot/file_config.rb +37 -0
- data/lib/termplot/message_broker.rb +108 -0
- data/lib/termplot/options.rb +100 -20
- data/lib/termplot/positioned_widget.rb +8 -0
- data/lib/termplot/producer_options.rb +3 -0
- data/lib/termplot/producers.rb +3 -3
- data/lib/termplot/producers/base_producer.rb +12 -15
- data/lib/termplot/producers/command_producer.rb +25 -9
- data/lib/termplot/producers/stdin_producer.rb +1 -4
- data/lib/termplot/renderable.rb +35 -0
- data/lib/termplot/renderer.rb +16 -257
- data/lib/termplot/renderers.rb +6 -0
- data/lib/termplot/renderers/border_renderer.rb +48 -0
- data/lib/termplot/renderers/text_renderer.rb +73 -0
- data/lib/termplot/shell.rb +13 -9
- data/lib/termplot/utils/ansi_safe_string.rb +68 -0
- data/lib/termplot/version.rb +1 -1
- data/lib/termplot/widget_dsl.rb +130 -0
- data/lib/termplot/widgets.rb +8 -0
- data/lib/termplot/widgets/base_widget.rb +79 -0
- data/lib/termplot/widgets/border.rb +6 -0
- data/lib/termplot/widgets/dataset.rb +50 -0
- data/lib/termplot/widgets/histogram_widget.rb +196 -0
- data/lib/termplot/widgets/statistics.rb +21 -0
- data/lib/termplot/widgets/statistics_widget.rb +104 -0
- data/lib/termplot/widgets/time_series_widget.rb +248 -0
- data/lib/termplot/window.rb +25 -5
- data/termplot.gemspec +1 -6
- metadata +36 -24
- data/doc/MSFT.png +0 -0
- data/doc/cpu.png +0 -0
- data/doc/demo.cast +0 -638
- data/lib/termplot/consumer.rb +0 -75
- data/lib/termplot/cursors/console_cursor.rb +0 -57
- data/lib/termplot/series.rb +0 -37
data/lib/termplot/renderer.rb
CHANGED
@@ -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
|
-
|
10
|
+
include Renderable
|
10
11
|
|
11
|
-
def initialize(cols: 80, rows: 20, debug: false)
|
12
|
-
|
13
|
-
|
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
|
19
|
+
def render_to_window
|
26
20
|
window.clear
|
27
21
|
errors.clear
|
28
22
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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 :
|
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,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
|
data/lib/termplot/shell.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require "
|
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
|
-
|
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
|
-
|
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
|
-
|
25
|
-
Termios.tcsetattr($stdout, Termios::TCSAFLUSH, termios_settings)
|
29
|
+
STDOUT.echo = true
|
26
30
|
|
27
31
|
print CURSOR_SHOW
|
28
32
|
end
|