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
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "termplot/dsl/panels"
|
4
|
+
|
5
|
+
module Termplot
|
6
|
+
class FileConfig
|
7
|
+
attr_reader :options, :rows, :cols, :widget_configs
|
8
|
+
def initialize(options)
|
9
|
+
@options = options
|
10
|
+
@path = options.file
|
11
|
+
@rows = options.rows
|
12
|
+
@cols = options.cols
|
13
|
+
@widget_configs = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_config
|
17
|
+
code = File.read(path)
|
18
|
+
top_level_panel = Termplot::DSL::Col.new(options)
|
19
|
+
top_level_panel.instance_eval(code)
|
20
|
+
|
21
|
+
@widget_configs = resolve_widget_positions(top_level_panel)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def positioned_widgets
|
26
|
+
widget_configs.map(&:positioned_widget)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
attr_reader :path
|
31
|
+
|
32
|
+
def resolve_widget_positions(top_level_panel)
|
33
|
+
top_level_panel.set_dimensions(rows, cols, 0, 0)
|
34
|
+
top_level_panel.flatten
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module Termplot
|
4
|
+
class MessageBrokerPool
|
5
|
+
def initialize
|
6
|
+
@brokers = []
|
7
|
+
@mutex = Mutex.new
|
8
|
+
@on_message_callbacks = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def broker(sender:, receiver:)
|
12
|
+
mutex.synchronize do
|
13
|
+
broker = MessageBroker.new(sender: sender, receiver: receiver)
|
14
|
+
broker.on_message do |v|
|
15
|
+
on_message_callbacks.each do |block|
|
16
|
+
block.call(v)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
brokers.push(broker)
|
20
|
+
broker
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_message(&block)
|
25
|
+
mutex.synchronize do
|
26
|
+
on_message_callbacks.push(block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def closed?
|
31
|
+
mutex.synchronize do
|
32
|
+
brokers.count > 0 &&
|
33
|
+
brokers.all?(:closed?)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def shutdown
|
38
|
+
mutex.synchronize do
|
39
|
+
brokers.each(&:close)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def flush_messages
|
44
|
+
mutex.synchronize do
|
45
|
+
brokers.each(&:flush_queue)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def pending_message_count
|
50
|
+
mutex.synchronize do
|
51
|
+
brokers.inject(0) do |sum, broker|
|
52
|
+
sum + broker.pending_message_count
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def empty?
|
58
|
+
pending_message_count == 0
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
attr_reader :brokers, :mutex, :on_message_callbacks
|
63
|
+
end
|
64
|
+
|
65
|
+
# Broker messages in a thread-safe way between a sender and a receiver.
|
66
|
+
class MessageBroker
|
67
|
+
def initialize(sender:, receiver:)
|
68
|
+
@sender = sender
|
69
|
+
@receiver = receiver
|
70
|
+
@queue = Queue.new
|
71
|
+
@on_message_callbacks = []
|
72
|
+
|
73
|
+
register_callbacks
|
74
|
+
end
|
75
|
+
|
76
|
+
def on_message(block = Proc.new)
|
77
|
+
on_message_callbacks.push(block)
|
78
|
+
end
|
79
|
+
|
80
|
+
def pending_message_count
|
81
|
+
queue.size
|
82
|
+
end
|
83
|
+
|
84
|
+
def flush_queue
|
85
|
+
num_samples = queue.size
|
86
|
+
num_samples.times do
|
87
|
+
receiver << queue.shift
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def close
|
92
|
+
queue.close
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
attr_reader :sender, :receiver, :queue, :on_message_callbacks
|
97
|
+
|
98
|
+
def register_callbacks
|
99
|
+
on_message_callbacks.push -> (value) { queue << value }
|
100
|
+
|
101
|
+
sender.on_message do |value|
|
102
|
+
on_message_callbacks.each do |block|
|
103
|
+
block.call(value)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/termplot/options.rb
CHANGED
@@ -3,31 +3,70 @@
|
|
3
3
|
require "optparse"
|
4
4
|
require "termplot/character_map"
|
5
5
|
require "termplot/colors"
|
6
|
+
require "termplot/producer_options"
|
7
|
+
require "termplot/shell"
|
6
8
|
|
7
9
|
module Termplot
|
8
10
|
class Options
|
9
11
|
attr_reader :rows,
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
:cols,
|
13
|
+
:full_screen,
|
14
|
+
:debug,
|
15
|
+
:file,
|
16
|
+
:command,
|
17
|
+
:interval,
|
18
|
+
:type,
|
19
|
+
:title,
|
20
|
+
:line_style,
|
21
|
+
:color
|
17
22
|
|
18
23
|
def initialize
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
self.class.default_options.each do |(option, value)|
|
25
|
+
instance_variable_set("@#{option}", value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# 3 input modes supported:
|
30
|
+
# - Read from stdin and render a single chart (default)
|
31
|
+
# - Run a single command at an interval and render a single chart
|
32
|
+
# - Read configuration from a file, run multiple commands at an interval and
|
33
|
+
# render multiple charts in a dashboard
|
34
|
+
def input_mode
|
35
|
+
return :file unless @file.nil?
|
36
|
+
return :command unless @command.nil?
|
37
|
+
:stdin
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.default_options
|
41
|
+
{
|
42
|
+
# General options
|
43
|
+
rows: 19,
|
44
|
+
cols: 100,
|
45
|
+
full_screen: false,
|
46
|
+
debug: false,
|
47
|
+
|
48
|
+
# Input modes
|
49
|
+
file: nil,
|
50
|
+
command: nil,
|
51
|
+
interval: 1000,
|
52
|
+
|
53
|
+
# Widget (only necessary for stdin/command input modes)
|
54
|
+
type: "timeseries",
|
55
|
+
|
56
|
+
# General - All/multiple widget types
|
57
|
+
title: "Series",
|
58
|
+
color: "green",
|
59
|
+
|
60
|
+
# Timeseries
|
61
|
+
line_style: "heavy-line",
|
62
|
+
}
|
27
63
|
end
|
28
64
|
|
29
|
-
def
|
30
|
-
|
65
|
+
def to_h
|
66
|
+
self.class.default_options.inject({}) do |hash, (k, _)|
|
67
|
+
hash[k] = instance_variable_get("@#{k}")
|
68
|
+
hash
|
69
|
+
end
|
31
70
|
end
|
32
71
|
|
33
72
|
def parse_options!
|
@@ -40,12 +79,19 @@ module Termplot
|
|
40
79
|
|
41
80
|
parse_rows(opts)
|
42
81
|
parse_cols(opts)
|
43
|
-
|
44
|
-
|
45
|
-
|
82
|
+
parse_full_screen(opts)
|
83
|
+
|
84
|
+
parse_file(opts)
|
46
85
|
parse_command(opts)
|
47
86
|
parse_interval(opts)
|
48
87
|
|
88
|
+
parse_type(opts)
|
89
|
+
|
90
|
+
parse_title(opts)
|
91
|
+
parse_color(opts)
|
92
|
+
|
93
|
+
parse_line_style(opts)
|
94
|
+
|
49
95
|
opts.on("-h", "--help", "Display this help message") do
|
50
96
|
puts opts
|
51
97
|
exit(0)
|
@@ -55,6 +101,10 @@ module Termplot
|
|
55
101
|
self
|
56
102
|
end
|
57
103
|
|
104
|
+
def producer_options
|
105
|
+
ProducerOptions.new(command: command, interval: interval)
|
106
|
+
end
|
107
|
+
|
58
108
|
private
|
59
109
|
|
60
110
|
def parse_rows(opts)
|
@@ -71,6 +121,20 @@ module Termplot
|
|
71
121
|
end
|
72
122
|
end
|
73
123
|
|
124
|
+
def parse_full_screen(opts)
|
125
|
+
opts.on("--full-screen", "Render to the full available terminal size") do |v|
|
126
|
+
@rows, @cols = Shell.get_dimensions
|
127
|
+
@full_screen = true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_file(opts)
|
132
|
+
opts.on("-f FILE", "--file FILE",
|
133
|
+
"Read a dashboard configuration from a file") do |v|
|
134
|
+
@file = v
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
74
138
|
def parse_title(opts)
|
75
139
|
opts.on("-tTITLE", "--title TITLE",
|
76
140
|
"Title of the series (default: '#{@title}')") do |v|
|
@@ -114,6 +178,22 @@ module Termplot
|
|
114
178
|
end
|
115
179
|
end
|
116
180
|
|
181
|
+
def parse_type(opts)
|
182
|
+
widget_types = %w( timeseries stats hist )
|
183
|
+
widget_types_with_default = with_default(widget_types, @type)
|
184
|
+
opts.on("--type TYPE",
|
185
|
+
"The type of chart to render. ",
|
186
|
+
"Options are: #{widget_types_with_default.join(", ")}") do |v|
|
187
|
+
@type = v
|
188
|
+
end
|
189
|
+
|
190
|
+
widget_types.each do |type|
|
191
|
+
opts.on("--#{type}", "Shorthand for --type #{type}") do |_|
|
192
|
+
@type = type
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
117
197
|
def parse_debug
|
118
198
|
if ARGV.delete("--debug") || ARGV.delete("-d")
|
119
199
|
@debug = true
|
data/lib/termplot/producers.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Termplot
|
2
2
|
module Producers
|
3
|
-
autoload :BaseProducer, "termplot/producers/base_producer
|
4
|
-
autoload :CommandProducer, "termplot/producers/command_producer
|
5
|
-
autoload :StdinProducer, "termplot/producers/stdin_producer
|
3
|
+
autoload :BaseProducer, "termplot/producers/base_producer"
|
4
|
+
autoload :CommandProducer, "termplot/producers/command_producer"
|
5
|
+
autoload :StdinProducer, "termplot/producers/stdin_producer"
|
6
6
|
end
|
7
7
|
end
|
@@ -1,31 +1,28 @@
|
|
1
1
|
module Termplot
|
2
2
|
module Producers
|
3
3
|
class BaseProducer
|
4
|
-
def initialize(
|
4
|
+
def initialize(options)
|
5
5
|
@options = options
|
6
|
-
@
|
7
|
-
@consumer = nil
|
6
|
+
@on_message_handler = -> {}
|
8
7
|
end
|
9
8
|
|
10
|
-
def
|
11
|
-
@
|
9
|
+
def on_message(&block)
|
10
|
+
@on_message_handler = block
|
12
11
|
end
|
13
12
|
|
14
|
-
def
|
15
|
-
|
13
|
+
def run
|
14
|
+
raise "Must be implemented"
|
16
15
|
end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
end
|
17
|
+
private
|
18
|
+
attr_reader :options, :on_message_handler
|
21
19
|
|
22
|
-
def
|
23
|
-
|
20
|
+
def produce(value)
|
21
|
+
if numeric?(value)
|
22
|
+
on_message_handler.call(value.to_f)
|
23
|
+
end
|
24
24
|
end
|
25
25
|
|
26
|
-
private
|
27
|
-
attr_reader :queue, :consumer, :options
|
28
|
-
|
29
26
|
FLOAT_REGEXP = /^[-+]?[0-9]*\.?[0-9]+$/
|
30
27
|
def numeric?(n)
|
31
28
|
n =~ FLOAT_REGEXP
|
@@ -1,26 +1,42 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
require "fileutils"
|
3
|
+
|
1
4
|
module Termplot
|
2
5
|
module Producers
|
3
6
|
class CommandProducer < BaseProducer
|
4
7
|
def run
|
5
|
-
|
8
|
+
temp_executable = make_executable(options.command)
|
6
9
|
loop do
|
7
|
-
n = `#{
|
10
|
+
n = `#{temp_executable.path}`.chomp
|
8
11
|
# TODO: Error handling...
|
9
12
|
|
10
|
-
|
11
|
-
queue << n.to_f
|
12
|
-
consumer&.run
|
13
|
-
end
|
13
|
+
produce(n)
|
14
14
|
|
15
15
|
# Interval is in ms
|
16
16
|
sleep(options.interval / 1000.0)
|
17
17
|
end
|
18
|
+
ensure
|
19
|
+
temp_executable.unlink
|
18
20
|
end
|
19
21
|
|
20
22
|
private
|
21
|
-
|
22
|
-
|
23
|
-
|
23
|
+
def make_executable(command)
|
24
|
+
file = Tempfile.new
|
25
|
+
file.write <<~COMMAND
|
26
|
+
#! #{ENV['SHELL']}
|
27
|
+
echo $(#{sanitize_command(command)})
|
28
|
+
COMMAND
|
29
|
+
file.close
|
30
|
+
|
31
|
+
FileUtils.chmod("a=xrw", file.path)
|
32
|
+
file
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: Proper sanitization, probably using Shellwords?
|
36
|
+
def sanitize_command(command)
|
37
|
+
# command.gsub('"', "'")
|
38
|
+
command
|
39
|
+
end
|
24
40
|
end
|
25
41
|
end
|
26
42
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Termplot
|
2
|
+
module Renderable
|
3
|
+
# Included in any module that has the ivars:
|
4
|
+
# :window
|
5
|
+
# :errors
|
6
|
+
# :debug
|
7
|
+
# And methods:
|
8
|
+
# #render_tO_window
|
9
|
+
# Provides rendering to string and stdout
|
10
|
+
def render
|
11
|
+
rendered_string = render_to_string
|
12
|
+
if debug?
|
13
|
+
rendered_string.each do |row|
|
14
|
+
print row
|
15
|
+
end
|
16
|
+
else
|
17
|
+
print rendered_string
|
18
|
+
STDOUT.flush
|
19
|
+
end
|
20
|
+
|
21
|
+
if errors.any?
|
22
|
+
window.print_errors(errors)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def render_to_string
|
27
|
+
render_to_window
|
28
|
+
debug? ? window.flush_debug : window.flush
|
29
|
+
end
|
30
|
+
|
31
|
+
def debug?
|
32
|
+
@debug
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|