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,128 @@
1
+ require "termplot/positioned_widget"
2
+ require "termplot/widgets"
3
+
4
+ module Termplot
5
+ module WidgetDSL
6
+ def timeseries(attrs)
7
+ attrs = merge_defaults(attrs)
8
+ children.push(TimeSeriesConfig.new(attrs))
9
+ end
10
+
11
+ def statistics(attrs)
12
+ attrs = merge_defaults(attrs)
13
+ children.push(StatisticsConfig.new(attrs))
14
+ end
15
+
16
+ def histogram(attrs)
17
+ attrs = merge_defaults(attrs)
18
+ children.push(HistogramConfig.new(attrs))
19
+ end
20
+
21
+ private
22
+
23
+ def merge_defaults(attrs)
24
+ Termplot::Options.default_options.merge(attrs)
25
+ end
26
+
27
+ class WidgetConfig
28
+ attr_reader(
29
+ :title,
30
+ :command,
31
+ :interval,
32
+ :col,
33
+ :row,
34
+ :cols,
35
+ :rows,
36
+ :debug
37
+ )
38
+
39
+ def initialize(opts)
40
+ @title = opts[:title]
41
+
42
+ @command = opts[:command]
43
+ @interval = opts[:interval]
44
+ @debug = opts[:debug]
45
+
46
+ post_initialize(opts)
47
+ end
48
+
49
+ def post_initialize(opts)
50
+ # Implemented in subclasses
51
+ end
52
+
53
+ def set_dimensions(rows, cols, start_row, start_col)
54
+ @rows = rows
55
+ @cols = cols
56
+ @row = start_row
57
+ @col = start_col
58
+ end
59
+
60
+ def flatten
61
+ self
62
+ end
63
+
64
+ def positioned_widget
65
+ @positioned_widget ||= PositionedWidget.new(
66
+ row: row,
67
+ col: col,
68
+ widget: widget
69
+ )
70
+ end
71
+
72
+ def widget
73
+ raise "Must be implemented"
74
+ end
75
+
76
+ def producer_options
77
+ ProducerOptions.new(command: command, interval: interval)
78
+ end
79
+ end
80
+
81
+ class TimeSeriesConfig < WidgetConfig
82
+ attr_reader :color, :line_style
83
+ def post_initialize(opts)
84
+ @color = opts[:color]
85
+ @line_style = opts[:line_style]
86
+ end
87
+
88
+ def widget
89
+ @widget ||= Termplot::Widgets::TimeSeriesWidget.new(
90
+ title: title,
91
+ line_style: line_style,
92
+ color: color,
93
+ cols: cols,
94
+ rows: rows,
95
+ debug: debug
96
+ )
97
+ end
98
+ end
99
+
100
+ class StatisticsConfig < WidgetConfig
101
+ def widget
102
+ @widget ||= Termplot::Widgets::StatisticsWidget.new(
103
+ title: title,
104
+ cols: cols,
105
+ rows: rows,
106
+ debug: debug
107
+ )
108
+ end
109
+ end
110
+
111
+ class HistogramConfig < WidgetConfig
112
+ attr_reader :color
113
+ def post_initialize(opts)
114
+ @color = opts[:color]
115
+ end
116
+
117
+ def widget
118
+ @widget ||= Termplot::Widgets::HistogramWidget.new(
119
+ title: title,
120
+ color: color,
121
+ cols: cols,
122
+ rows: rows,
123
+ debug: debug
124
+ )
125
+ end
126
+ end
127
+ end
128
+ end
@@ -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,111 @@
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) || brokers.all?(&:closed?)
33
+ end
34
+ end
35
+
36
+ def shutdown
37
+ mutex.synchronize do
38
+ brokers.each(&:close)
39
+ end
40
+ end
41
+
42
+ def flush_messages
43
+ mutex.synchronize do
44
+ brokers.each(&:flush_queue)
45
+ end
46
+ end
47
+
48
+ def pending_message_count
49
+ mutex.synchronize do
50
+ brokers.inject(0) do |sum, broker|
51
+ sum + broker.pending_message_count
52
+ end
53
+ end
54
+ end
55
+
56
+ def empty?
57
+ pending_message_count == 0
58
+ end
59
+
60
+ private
61
+ attr_reader :brokers, :mutex, :on_message_callbacks
62
+ end
63
+
64
+ # Broker messages in a thread-safe way between a sender and a receiver.
65
+ class MessageBroker
66
+ def initialize(sender:, receiver:)
67
+ @sender = sender
68
+ @receiver = receiver
69
+ @queue = Queue.new
70
+ @on_message_callbacks = []
71
+
72
+ register_callbacks
73
+ end
74
+
75
+ def on_message(&block)
76
+ on_message_callbacks.push(block)
77
+ end
78
+
79
+ def pending_message_count
80
+ queue.size
81
+ end
82
+
83
+ def flush_queue
84
+ num_samples = queue.size
85
+ num_samples.times do
86
+ receiver << queue.shift
87
+ end
88
+ end
89
+
90
+ def close
91
+ queue.close
92
+ end
93
+
94
+ def closed?
95
+ queue.closed?
96
+ end
97
+
98
+ private
99
+ attr_reader :sender, :receiver, :queue, :on_message_callbacks
100
+
101
+ def register_callbacks
102
+ on_message_callbacks.push -> (value) { queue << value }
103
+
104
+ sender.on_message do |value|
105
+ on_message_callbacks.each do |block|
106
+ block.call(value)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "termplot/character_map"
5
+ require "termplot/colors"
6
+ require "termplot/producer_options"
7
+ require "termplot/shell"
8
+
9
+ module Termplot
10
+ class Options
11
+ attr_reader :rows,
12
+ :cols,
13
+ :full_screen,
14
+ :debug,
15
+ :file,
16
+ :command,
17
+ :interval,
18
+ :type,
19
+ :title,
20
+ :line_style,
21
+ :color
22
+
23
+ def initialize
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
+ }
63
+ end
64
+
65
+ def to_h
66
+ self.class.default_options.inject({}) do |hash, (k, _)|
67
+ hash[k] = instance_variable_get("@#{k}")
68
+ hash
69
+ end
70
+ end
71
+
72
+ def parse_options!
73
+ # Debug option is parsed manually to prevent it from showing up in the
74
+ # options help
75
+ parse_debug
76
+
77
+ OptionParser.new do |opts|
78
+ opts.banner = "Usage: termplot [OPTIONS]"
79
+
80
+ parse_rows(opts)
81
+ parse_cols(opts)
82
+ parse_full_screen(opts)
83
+
84
+ parse_file(opts)
85
+ parse_command(opts)
86
+ parse_interval(opts)
87
+
88
+ parse_type(opts)
89
+
90
+ parse_title(opts)
91
+ parse_color(opts)
92
+
93
+ parse_line_style(opts)
94
+
95
+ opts.on("-h", "--help", "Display this help message") do
96
+ puts opts
97
+ exit(0)
98
+ end
99
+
100
+ end.parse!
101
+ self
102
+ end
103
+
104
+ def producer_options
105
+ ProducerOptions.new(command: command, interval: interval)
106
+ end
107
+
108
+ private
109
+
110
+ def parse_rows(opts)
111
+ opts.on("-r ROWS", "--rows ROWS",
112
+ "Number of rows in the chart window (default: #{@rows})") do |v|
113
+ @rows = v.to_i
114
+ end
115
+ end
116
+
117
+ def parse_cols(opts)
118
+ opts.on("-c COLS", "--cols COLS",
119
+ "Number of cols in the chart window (default: #{@cols})") do |v|
120
+ @cols = v.to_i
121
+ end
122
+ end
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
+
138
+ def parse_title(opts)
139
+ opts.on("-tTITLE", "--title TITLE",
140
+ "Title of the series (default: '#{@title}')") do |v|
141
+ @title = v
142
+ end
143
+ end
144
+
145
+ def parse_line_style(opts)
146
+ line_style_opts = with_default(Termplot::CharacterMap::LINE_STYLES.keys, @line_style)
147
+ opts.on("--line-style STYLE",
148
+ "Line style. Options are: #{line_style_opts.join(", ")}") do |v|
149
+ @line_style = v.downcase
150
+ end
151
+ end
152
+
153
+ def parse_color(opts)
154
+ color_opts = Termplot::Colors::COLORS.keys.map(&:to_s).reject do |c|
155
+ c == :default
156
+ end
157
+ color_opts = with_default(color_opts, @color)
158
+ opts.on("--color COLOR",
159
+ "Series color, specified as ansi 16-bit color name:",
160
+ "(i.e. #{color_opts.join(", ")})") do |v|
161
+ @color = v.downcase
162
+ end
163
+ end
164
+
165
+ def parse_command(opts)
166
+ opts.on("--command COMMAND",
167
+ "Enables command mode, where input is received by executing",
168
+ "the specified command in intervals rather than from stdin") do |v|
169
+ @command = v
170
+ end
171
+ end
172
+
173
+ def parse_interval(opts)
174
+ opts.on("--interval INTERVAL",
175
+ "The interval at which to run the specified command in",
176
+ "command mode in milliseconds (default: #{@interval})") do |v|
177
+ @interval = v.to_i
178
+ end
179
+ end
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
+
197
+ def parse_debug
198
+ if ARGV.delete("--debug") || ARGV.delete("-d")
199
+ @debug = true
200
+ end
201
+ end
202
+
203
+ def with_default(opt_arr, default)
204
+ opt_arr.map do |opt|
205
+ opt == default ?
206
+ opt + " (default)" :
207
+ opt
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,8 @@
1
+ require "forwardable"
2
+
3
+ module Termplot
4
+ PositionedWidget = Struct.new(:row, :col, :widget, keyword_init: true) do
5
+ extend Forwardable
6
+ def_delegators :widget, :window, :errors, :render_to_window
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ module Termplot
2
+ ProducerOptions = Struct.new(:command, :interval, keyword_init: true)
3
+ end
@@ -0,0 +1,32 @@
1
+ module Termplot
2
+ module Producers
3
+ class BaseProducer
4
+ def initialize(options)
5
+ @options = options
6
+ @on_message_handler = -> {}
7
+ end
8
+
9
+ def on_message(&block)
10
+ @on_message_handler = block
11
+ end
12
+
13
+ def run
14
+ raise "Must be implemented"
15
+ end
16
+
17
+ private
18
+ attr_reader :options, :on_message_handler
19
+
20
+ def produce(value)
21
+ if numeric?(value)
22
+ on_message_handler.call(value.to_f)
23
+ end
24
+ end
25
+
26
+ FLOAT_REGEXP = /^[-+]?[0-9]*\.?[0-9]+$/
27
+ def numeric?(n)
28
+ n =~ FLOAT_REGEXP
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ require "tempfile"
2
+ require "fileutils"
3
+
4
+ module Termplot
5
+ module Producers
6
+ class CommandProducer < BaseProducer
7
+ def run
8
+ temp_executable = make_executable(options.command)
9
+ loop do
10
+ n = `#{temp_executable.path}`.chomp
11
+ # TODO: Error handling...
12
+
13
+ produce(n)
14
+
15
+ # Interval is in ms
16
+ sleep(options.interval / 1000.0)
17
+ end
18
+ ensure
19
+ temp_executable.unlink
20
+ end
21
+
22
+ private
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
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ module Termplot
2
+ module Producers
3
+ class StdinProducer < BaseProducer
4
+ def run
5
+ while n = STDIN.gets&.chomp do
6
+ produce(n)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Termplot
2
+ module Producers
3
+ autoload :BaseProducer, "termplot/producers/base_producer"
4
+ autoload :CommandProducer, "termplot/producers/command_producer"
5
+ autoload :StdinProducer, "termplot/producers/stdin_producer"
6
+ end
7
+ 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