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
@@ -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
@@ -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
- :cols,
11
- :title,
12
- :line_style,
13
- :color,
14
- :debug,
15
- :command,
16
- :interval
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
- @rows = 19
20
- @cols = 80
21
- @title = "Series"
22
- @line_style = "line"
23
- @color = "red"
24
- @debug = false
25
- @command = nil
26
- @interval = 1000
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 mode
30
- @command.nil? ? :stdin : :command
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
- parse_title(opts)
44
- parse_line_style(opts)
45
- parse_color(opts)
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
@@ -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
@@ -1,7 +1,7 @@
1
1
  module Termplot
2
2
  module Producers
3
- autoload :BaseProducer, "termplot/producers/base_producer.rb"
4
- autoload :CommandProducer, "termplot/producers/command_producer.rb"
5
- autoload :StdinProducer, "termplot/producers/stdin_producer.rb"
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(queue, options)
4
+ def initialize(options)
5
5
  @options = options
6
- @queue = queue
7
- @consumer = nil
6
+ @on_message_handler = -> {}
8
7
  end
9
8
 
10
- def register_consumer(consumer)
11
- @consumer = consumer
9
+ def on_message(&block)
10
+ @on_message_handler = block
12
11
  end
13
12
 
14
- def shift
15
- queue.shift
13
+ def run
14
+ raise "Must be implemented"
16
15
  end
17
16
 
18
- def closed?
19
- queue.closed?
20
- end
17
+ private
18
+ attr_reader :options, :on_message_handler
21
19
 
22
- def close
23
- queue.close
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
- command = sanitize_command("/bin/bash -c '#{options.command}'")
8
+ temp_executable = make_executable(options.command)
6
9
  loop do
7
- n = `#{command}`
10
+ n = `#{temp_executable.path}`.chomp
8
11
  # TODO: Error handling...
9
12
 
10
- if numeric?(n)
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
- def sanitize_command(command)
22
- command.gsub(/\$/, '\\$')
23
- end
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
@@ -3,10 +3,7 @@ module Termplot
3
3
  class StdinProducer < BaseProducer
4
4
  def run
5
5
  while n = STDIN.gets&.chomp do
6
- if numeric?(n)
7
- queue << n.to_f
8
- consumer&.run
9
- end
6
+ produce(n)
10
7
  end
11
8
  end
12
9
  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