whirled_peas 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'whirled_peas'
5
+
6
+ require 'pry'
7
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,46 @@
1
+ require 'logger'
2
+
3
+ require 'whirled_peas/frame'
4
+ require 'whirled_peas/ui'
5
+ require 'whirled_peas/version'
6
+
7
+ module WhirledPeas
8
+ class Error < StandardError; end
9
+
10
+ DEFAULT_HOST = 'localhost'
11
+ DEFAULT_PORT = 8765
12
+ DEFAULT_REFRESH_RATE = 30
13
+
14
+
15
+ def self.start(driver, template_factory, log_level: Logger::INFO, refresh_rate: DEFAULT_REFRESH_RATE, host: DEFAULT_HOST, port: DEFAULT_PORT)
16
+ logger = Logger.new(File.open('whirled_peas.log', 'a'))
17
+ logger.level = log_level
18
+ logger.formatter = proc do |severity, datetime, progname, msg|
19
+ "[#{severity}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%L')} (#{progname}) - #{msg}\n"
20
+ end
21
+
22
+ consumer = Frame::Consumer.new(template_factory, refresh_rate, logger)
23
+ consumer_thread = Thread.new { consumer.start(host: host, port: port) }
24
+
25
+ Frame::Producer.start(logger: logger, host: host, port: port) do |producer|
26
+ begin
27
+ driver.start(producer)
28
+ producer.stop
29
+ rescue => e
30
+ logger.warn('MAIN') { "Driver exited with error, terminating producer..." }
31
+ logger.error('MAIN') { e }
32
+ logger.error('MAIN') { e.backtrace.join("\n") }
33
+ producer.terminate
34
+ raise
35
+ end
36
+ end
37
+
38
+ consumer_thread.join
39
+ end
40
+
41
+ def self.template(&block)
42
+ template = UI::Template.new
43
+ yield template, template.settings
44
+ template
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'frame/consumer'
2
+ require_relative 'frame/producer'
3
+
4
+ module WhirledPeas
5
+ module Frame
6
+ TERMINATE = '__term__'
7
+ EOF = '__EOF__'
8
+
9
+ DEFAULT_ADDRESS = 'localhost'
10
+ DEFAULT_PORT = 8765
11
+ end
12
+
13
+ private_constant :Frame
14
+ end
@@ -0,0 +1,61 @@
1
+ require 'socket'
2
+ require 'json'
3
+
4
+ require_relative 'loop'
5
+
6
+ module WhirledPeas
7
+ module Frame
8
+ class Consumer
9
+ def initialize(template_factory, refresh_rate, logger=NullLogger.new)
10
+ @loop = Loop.new(template_factory, refresh_rate, logger)
11
+ @logger = logger
12
+ @running = false
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def start(host:, port:)
17
+ mutex.synchronize { @running = true }
18
+ loop_thread = Thread.new { loop.start }
19
+ socket = TCPSocket.new(host, port)
20
+ logger.info('CONSUMER') { "Connected to #{host}:#{port}" }
21
+ while @running
22
+ line = socket.gets
23
+ if line.nil?
24
+ sleep(0.001)
25
+ next
26
+ end
27
+ args = JSON.parse(line)
28
+ name = args.delete('name')
29
+ if [Frame::EOF, Frame::TERMINATE].include?(name)
30
+ logger.info('CONSUMER') { "Received #{name} event, stopping..." }
31
+ loop.stop if name == Frame::TERMINATE
32
+ @running = false
33
+ else
34
+ duration = args.delete('duration')
35
+ loop.enqueue(name, duration, args)
36
+ end
37
+ end
38
+ logger.info('CONSUMER') { "Exited normally" }
39
+ rescue => e
40
+ logger.warn('CONSUMER') { "Exited with error" }
41
+ logger.error('CONSUMER') { e.message }
42
+ logger.error('CONSUMER') { e.backtrace.join("\n") }
43
+ loop.stop
44
+ ensure
45
+ logger.info('CONSUMER') { "Waiting for loop thread to exit" }
46
+ loop_thread.join
47
+ logger.info('CONSUMER') { "Closing socket" }
48
+ socket.close if socket
49
+ end
50
+
51
+ def stop
52
+ logger.info('CONSUMER') { "Stopping..." }
53
+ mutex.synchronize { @running = false }
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :loop, :logger, :mutex
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,56 @@
1
+ module WhirledPeas
2
+ module Frame
3
+ class Loop
4
+ def initialize(template_factory, refresh_rate, logger=NullLogger.new)
5
+ @template_factory = template_factory
6
+ @queue = Queue.new
7
+ @refresh_rate = refresh_rate
8
+ @logger = logger
9
+ end
10
+
11
+ def enqueue(name, duration, args)
12
+ queue.push([name, duration, args])
13
+ end
14
+
15
+ def start
16
+ logger.info('EVENT LOOP') { "Starting" }
17
+ @running = true
18
+ screen = UI::Screen.new
19
+ sleep(0.01) while queue.empty? # Wait for the first event
20
+ remaining_frames = 1
21
+ template = nil
22
+ while @running && remaining_frames > 0
23
+ frame_at = Time.now
24
+ next_frame_at = frame_at + 1.0 / refresh_rate
25
+ remaining_frames -= 1
26
+ if remaining_frames > 0
27
+ screen.refresh if screen.needs_refresh?
28
+ elsif !queue.empty?
29
+ name, duration, args = queue.pop
30
+ remaining_frames = duration ? duration * refresh_rate : 1
31
+ template = template_factory.build(name, args)
32
+ screen.paint(template)
33
+ end
34
+ sleep(next_frame_at - Time.now)
35
+ end
36
+ logger.info('EVENT LOOP') { "Exiting normally" }
37
+ rescue => e
38
+ logger.warn('EVENT LOOP') { "Exiting with error" }
39
+ logger.error('EVENT LOOP') { e.message }
40
+ logger.error('EVENT LOOP') { e.backtrace.join("\n") }
41
+ ensure
42
+ screen.finalize if screen
43
+ end
44
+
45
+ def stop
46
+ logger.info('EVENT LOOP') { "Stopping..." }
47
+ @running = false
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :template_factory, :queue, :refresh_rate, :logger
53
+ end
54
+ private_constant :Loop
55
+ end
56
+ end
@@ -0,0 +1,57 @@
1
+ require 'socket'
2
+ require 'json'
3
+
4
+ module WhirledPeas
5
+ module Frame
6
+ class Producer
7
+ def self.start(logger: NullLogger.new, host:, port:, &block)
8
+ server = TCPServer.new(host, port)
9
+ client = server.accept
10
+ logger.info('PRODUCER') { "Connected to #{host}:#{port}" }
11
+ producer = new(client, logger)
12
+ yield producer
13
+ logger.info('PRODUCER') { "Exited normally" }
14
+ rescue => e
15
+ logger.warn('PRODUCER') { "Exited with error" }
16
+ logger.error('PRODUCER') { e.message }
17
+ logger.error('PRODUCER') { e.backtrace.join("\n") }
18
+ ensure
19
+ client.close if client
20
+ end
21
+
22
+ def initialize(client, logger=NullLogger.new)
23
+ @client = client
24
+ @logger = logger
25
+ @queue = Queue.new
26
+ end
27
+
28
+ def send(name, duration: nil, args: {})
29
+ client.puts(JSON.generate('name' => name, 'duration' => duration, **args))
30
+ logger.debug('PRODUCER') { "Sending frame: #{name}" }
31
+ end
32
+
33
+ def enqueue(name, duration: nil, args: {})
34
+ queue.push([name, duration, args])
35
+ end
36
+
37
+ def flush
38
+ while !queue.empty?
39
+ name, duration, args = queue.pop
40
+ send(name, duration: duration, args: args)
41
+ end
42
+ end
43
+
44
+ def stop
45
+ send(Frame::EOF)
46
+ end
47
+
48
+ def terminate
49
+ send(Frame::TERMINATE)
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :client, :logger, :queue
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,20 @@
1
+ module WhirledPeas
2
+ class NullLogger
3
+ def debug(*)
4
+ end
5
+
6
+ def error(*)
7
+ end
8
+
9
+ def fatal(*)
10
+ end
11
+
12
+ def info(*)
13
+ end
14
+
15
+ def warn(*)
16
+ end
17
+ end
18
+
19
+ private_constant :NullLogger
20
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'ui/element'
2
+ require_relative 'ui/screen'
3
+
4
+ module WhirledPeas
5
+ module UI
6
+ end
7
+ end
@@ -0,0 +1,154 @@
1
+ module WhirledPeas
2
+ module UI
3
+ DEBUG_COLOR = ARGV.include?('--debug-color')
4
+
5
+ module Ansi
6
+ BOLD = 1
7
+ UNDERLINE = 4
8
+
9
+ BLACK = 30
10
+ RED = 31
11
+ GREEN = 32
12
+ YELLOW = 33
13
+ BLUE = 34
14
+ MAGENTA = 35
15
+ CYAN = 36
16
+ WHITE = 37
17
+
18
+ END_FORMATTING = 0
19
+
20
+ class << self
21
+ def format(str, codes)
22
+ if str.empty? || codes.length == 0
23
+ str
24
+ else
25
+ start_formatting = codes.map(&method(:esc_seq)).join
26
+ "#{start_formatting}#{str}#{esc_seq(END_FORMATTING)}"
27
+ end
28
+ end
29
+
30
+ def clear
31
+ esc_seq(END_FORMATTING)
32
+ end
33
+
34
+ def hidden_width(line)
35
+ return 0 if DEBUG_COLOR
36
+ width = 0
37
+ line.scan(/\033\[\d+m/).each { |f| width += f.length }
38
+ width
39
+ end
40
+
41
+ def close_formatting(line)
42
+ codes = line.scan(DEBUG_COLOR ? /<(\d+)>/ : /\033\[(\d+)m/)
43
+ if codes.length > 0 && codes.last[0] != END_FORMATTING.to_s
44
+ "#{line}#{esc_seq(END_FORMATTING)}"
45
+ else
46
+ line
47
+ end
48
+ end
49
+
50
+ def first(str, num_visible_chars)
51
+ return str if str.length <= num_visible_chars + hidden_width(str)
52
+ result = ''
53
+ in_format = false
54
+ visible_len = 0
55
+ str.chars.each do |char|
56
+ in_format = true if !in_format && char == "\033"
57
+ result += char
58
+ visible_len += 1 if !in_format
59
+ in_format = false if in_format && char == 'm'
60
+ break if visible_len == num_visible_chars
61
+ end
62
+ close_formatting(result)
63
+ end
64
+
65
+ private
66
+
67
+ def esc_seq(code)
68
+ DEBUG_COLOR ? "<#{code}>" : "\033[#{code}m"
69
+ end
70
+ end
71
+ end
72
+
73
+ class Color
74
+ BRIGHT_OFFSET = 60
75
+ private_constant :BRIGHT_OFFSET
76
+
77
+ def self.validate!(color)
78
+ return unless color
79
+ if color.is_a?(Symbol)
80
+ error_message = "Unsupported #{self.name.split('::').last}: #{color}"
81
+ match = color.to_s.match(/^(bright_)?(\w+)$/)
82
+ begin
83
+ color = self.const_get(match[2].upcase)
84
+ raise ArgumentError, error_message unless color.is_a?(Color)
85
+ if match[1]
86
+ raise ArgumentError, error_message if color.bright?
87
+ color.bright
88
+ else
89
+ color
90
+ end
91
+ rescue NameError
92
+ raise ArgumentError, error_message
93
+ end
94
+ else
95
+ color
96
+ end
97
+ end
98
+
99
+ def initialize(code, bright=false)
100
+ @code = code
101
+ @bright = bright
102
+ end
103
+
104
+ def bright?
105
+ @bright
106
+ end
107
+
108
+ def bright
109
+ bright? ? self : self.class.new(@code + BRIGHT_OFFSET, true)
110
+ end
111
+
112
+ def to_s
113
+ @code.to_s
114
+ end
115
+
116
+ def inspect
117
+ "#{self.class.name.split('::').last}(code=#{@code}, bright=#{@bright})"
118
+ end
119
+ end
120
+ private_constant :Color
121
+
122
+ class BgColor < Color
123
+ BG_OFFSET = 10
124
+ private_constant :BG_OFFSET
125
+
126
+ BLACK = new(Ansi::BLACK + BG_OFFSET)
127
+ RED = new(Ansi::RED + BG_OFFSET)
128
+ GREEN = new(Ansi::GREEN + BG_OFFSET)
129
+ YELLOW = new(Ansi::YELLOW + BG_OFFSET)
130
+ BLUE = new(Ansi::BLUE + BG_OFFSET)
131
+ MAGENTA = new(Ansi::MAGENTA + BG_OFFSET)
132
+ CYAN = new(Ansi::CYAN + BG_OFFSET)
133
+ WHITE = new(Ansi::WHITE + BG_OFFSET)
134
+ GRAY = BLACK.bright
135
+ end
136
+
137
+ class TextColor < Color
138
+ BLACK = new(Ansi::BLACK)
139
+ RED = new(Ansi::RED)
140
+ GREEN = new(Ansi::GREEN)
141
+ YELLOW = new(Ansi::YELLOW)
142
+ BLUE = new(Ansi::BLUE)
143
+ MAGENTA = new(Ansi::MAGENTA)
144
+ CYAN = new(Ansi::CYAN)
145
+ WHITE = new(Ansi::WHITE)
146
+ GRAY = BLACK.bright
147
+ end
148
+
149
+ module TextFormat
150
+ BOLD = Ansi::BOLD
151
+ UNDERLINE = Ansi::UNDERLINE
152
+ end
153
+ end
154
+ end