whirled_peas 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +125 -6
- data/bin/title_fonts +6 -0
- data/exe/whirled_peas +7 -0
- data/lib/whirled_peas.rb +12 -32
- data/lib/whirled_peas/command_line.rb +266 -0
- data/lib/whirled_peas/config.rb +21 -0
- data/lib/whirled_peas/errors.rb +5 -0
- data/lib/whirled_peas/frame.rb +0 -4
- data/lib/whirled_peas/frame/event_loop.rb +60 -28
- data/lib/whirled_peas/frame/print_consumer.rb +33 -0
- data/lib/whirled_peas/frame/producer.rb +29 -18
- data/lib/whirled_peas/template.rb +5 -0
- data/lib/whirled_peas/{ui → template}/element.rb +12 -6
- data/lib/whirled_peas/{ui → template}/settings.rb +8 -1
- data/lib/whirled_peas/ui.rb +1 -3
- data/lib/whirled_peas/ui/canvas.rb +1 -1
- data/lib/whirled_peas/ui/painter.rb +20 -16
- data/lib/whirled_peas/ui/screen.rb +21 -18
- data/lib/whirled_peas/utils.rb +5 -0
- data/lib/whirled_peas/{ui → utils}/ansi.rb +0 -0
- data/lib/whirled_peas/{ui → utils}/color.rb +0 -0
- data/lib/whirled_peas/utils/title_font.rb +75 -0
- data/lib/whirled_peas/version.rb +1 -1
- data/whirled_peas.gemspec +4 -1
- metadata +32 -13
- data/sandbox/auto.rb +0 -13
- data/sandbox/box.rb +0 -19
- data/sandbox/grid.rb +0 -13
- data/sandbox/sandbox.rb +0 -17
- data/sandbox/text.rb +0 -33
@@ -0,0 +1,21 @@
|
|
1
|
+
module WhirledPeas
|
2
|
+
class Config
|
3
|
+
attr_writer :driver, :template_factory
|
4
|
+
attr_accessor :loading_template_factory
|
5
|
+
|
6
|
+
def driver
|
7
|
+
unless @driver
|
8
|
+
raise ConfigurationError, 'driver must be configured'
|
9
|
+
end
|
10
|
+
@driver
|
11
|
+
end
|
12
|
+
|
13
|
+
def template_factory
|
14
|
+
unless @template_factory
|
15
|
+
raise ConfigurationError, 'template_factory must be configured'
|
16
|
+
end
|
17
|
+
@template_factory
|
18
|
+
end
|
19
|
+
end
|
20
|
+
private_constant :Config
|
21
|
+
end
|
data/lib/whirled_peas/frame.rb
CHANGED
@@ -1,15 +1,22 @@
|
|
1
|
+
require_relative '../null_logger'
|
2
|
+
require_relative '../ui/screen'
|
3
|
+
|
1
4
|
module WhirledPeas
|
2
5
|
module Frame
|
3
6
|
class EventLoop
|
4
|
-
|
7
|
+
LOGGER_ID = 'EVENT LOOP'
|
8
|
+
|
9
|
+
def initialize(template_factory, loading_template_factory, refresh_rate, logger=NullLogger.new)
|
5
10
|
@template_factory = template_factory
|
11
|
+
@loading_template_factory = loading_template_factory
|
6
12
|
@queue = Queue.new
|
7
|
-
@
|
13
|
+
@frame_duration = 1.0 / refresh_rate
|
8
14
|
@logger = logger
|
9
15
|
end
|
10
16
|
|
11
|
-
def enqueue(name
|
12
|
-
|
17
|
+
def enqueue(name, duration, args)
|
18
|
+
# If duration is nil, set it to the duration of a single frame
|
19
|
+
queue.push([name, duration || frame_duration, args])
|
13
20
|
end
|
14
21
|
|
15
22
|
def running?
|
@@ -17,43 +24,68 @@ module WhirledPeas
|
|
17
24
|
end
|
18
25
|
|
19
26
|
def start
|
20
|
-
logger.info('EVENT LOOP') { 'Starting' }
|
21
|
-
@running = true
|
22
27
|
screen = UI::Screen.new
|
23
|
-
|
24
|
-
|
25
|
-
template = nil
|
26
|
-
while @running && remaining_frames > 0
|
27
|
-
frame_at = Time.now
|
28
|
-
next_frame_at = frame_at + 1.0 / refresh_rate
|
29
|
-
remaining_frames -= 1
|
30
|
-
if remaining_frames > 0
|
31
|
-
screen.refresh if screen.needs_refresh?
|
32
|
-
elsif !queue.empty?
|
33
|
-
name, duration, args = queue.pop
|
34
|
-
remaining_frames = duration ? duration * refresh_rate : 1
|
35
|
-
template = template_factory.build(name, args)
|
36
|
-
screen.paint(template)
|
37
|
-
end
|
38
|
-
sleep([0, next_frame_at - Time.now].max)
|
39
|
-
end
|
40
|
-
logger.info('EVENT LOOP') { 'Exiting normally' }
|
28
|
+
wait_for_content(screen)
|
29
|
+
play_content(screen)
|
41
30
|
rescue
|
42
|
-
logger.warn(
|
43
|
-
@running = false
|
31
|
+
logger.warn(LOGGER_ID) { 'Exiting with error' }
|
44
32
|
raise
|
45
33
|
ensure
|
34
|
+
# We may have exited due to an EOF or a raised exception, set state so that
|
35
|
+
# instance reflects actual state.
|
36
|
+
@running = false
|
46
37
|
screen.finalize if screen
|
47
38
|
end
|
48
39
|
|
49
40
|
def stop
|
50
|
-
logger.info(
|
41
|
+
logger.info(LOGGER_ID) { 'Stopping...' }
|
51
42
|
@running = false
|
52
43
|
end
|
53
44
|
|
54
45
|
private
|
55
46
|
|
56
|
-
attr_reader :template_factory, :queue, :
|
47
|
+
attr_reader :template_factory, :loading_template_factory, :queue, :frame_duration, :logger
|
48
|
+
|
49
|
+
def wait_for_content(screen)
|
50
|
+
if loading_template_factory
|
51
|
+
play_loading_screen(screen)
|
52
|
+
else
|
53
|
+
sleep(frame_duration) while queue.empty?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def play_loading_screen(screen)
|
58
|
+
while queue.empty?
|
59
|
+
screen.paint(loading_template_factory.build)
|
60
|
+
sleep(frame_duration)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def play_content(screen)
|
65
|
+
@running = true
|
66
|
+
template = nil
|
67
|
+
frame_until = Time.new(0) # Tell the loop to immediately pick up a new frame
|
68
|
+
while running?
|
69
|
+
frame_start = Time.now
|
70
|
+
next_frame_at = frame_start + frame_duration
|
71
|
+
if frame_until > frame_start
|
72
|
+
# While we're still displaying the previous frame, refresh the screen
|
73
|
+
screen.refresh
|
74
|
+
elsif !queue.empty?
|
75
|
+
name, duration, args = queue.pop
|
76
|
+
if name == Frame::EOF
|
77
|
+
@running = false
|
78
|
+
else
|
79
|
+
frame_until = frame_start + duration
|
80
|
+
template = template_factory.build(name, args)
|
81
|
+
screen.paint(template)
|
82
|
+
end
|
83
|
+
else
|
84
|
+
wait_for_content(screen)
|
85
|
+
end
|
86
|
+
sleep([0, next_frame_at - Time.now].max)
|
87
|
+
end
|
88
|
+
end
|
57
89
|
end
|
58
90
|
end
|
59
91
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative '../null_logger'
|
2
|
+
|
3
|
+
module WhirledPeas
|
4
|
+
module Frame
|
5
|
+
class PrintConsumer
|
6
|
+
LOGGER_ID = 'EVENT LOOP'
|
7
|
+
|
8
|
+
def initialize(logger=NullLogger.new)
|
9
|
+
@logger = logger
|
10
|
+
end
|
11
|
+
|
12
|
+
def enqueue(name, duration, args)
|
13
|
+
if name == Frame::EOF
|
14
|
+
puts "EOF frame detected"
|
15
|
+
else
|
16
|
+
displayed_for = duration ? "#{duration} second(s)" : '1 frame'
|
17
|
+
args_str = args.empty? ? '' : " (#{args.inspect})"
|
18
|
+
puts "Frame '#{name}' displayed for #{displayed_for}#{args_str}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :logger
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,50 +1,61 @@
|
|
1
1
|
require 'socket'
|
2
2
|
require 'json'
|
3
3
|
|
4
|
+
require_relative '../null_logger'
|
5
|
+
|
4
6
|
module WhirledPeas
|
5
7
|
module Frame
|
6
8
|
class Producer
|
7
9
|
LOGGER_ID = 'PRODUCER'
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
# Manages the EventLoop lifecycle and yields a Producer to send frames to the
|
12
|
+
# EventLoop
|
13
|
+
def self.produce(consumer, logger=NullLogger.new)
|
14
|
+
producer = new(consumer, logger)
|
15
|
+
consumer_thread = Thread.new do
|
16
|
+
Thread.current.report_on_exception = false
|
17
|
+
consumer.start
|
18
|
+
end
|
12
19
|
yield producer
|
13
|
-
logger.info(LOGGER_ID) { 'Done with yield' }
|
14
20
|
producer.send_frame(Frame::EOF)
|
15
|
-
|
21
|
+
producer.flush
|
16
22
|
rescue => e
|
17
|
-
|
23
|
+
consumer.stop if consumer
|
18
24
|
logger.warn(LOGGER_ID) { 'Exited with error' }
|
19
25
|
logger.error(LOGGER_ID) { e }
|
20
26
|
raise
|
27
|
+
ensure
|
28
|
+
consumer_thread.join if consumer_thread
|
21
29
|
end
|
22
30
|
|
23
|
-
def initialize(
|
24
|
-
@
|
31
|
+
def initialize(consumer, logger=NullLogger.new)
|
32
|
+
@consumer = consumer
|
25
33
|
@logger = logger
|
26
34
|
@queue = Queue.new
|
27
35
|
end
|
28
36
|
|
37
|
+
# Buffer a frame to be played for the given duration. `#flush` must be called
|
38
|
+
# for frames to get pushed to the EventLoop.
|
39
|
+
#
|
40
|
+
# @param name [String] name of frame, which is passed to #build of the
|
41
|
+
# TemplateFactory
|
42
|
+
# @param duration [Float|Integer] duration in seconds the frame should be,
|
43
|
+
# displayed (default is nil, which results in a duration of a single refresh
|
44
|
+
# cycle)
|
45
|
+
# @param args [Hash] key/value pair of arguments, which is passed to #build of
|
46
|
+
# the TemplateFactory
|
29
47
|
def send_frame(name, duration: nil, args: {})
|
30
|
-
event_loop.enqueue(name: name, duration: duration, args: args)
|
31
|
-
logger.debug(LOGGER_ID) { "Sending frame: #{name}" }
|
32
|
-
end
|
33
|
-
|
34
|
-
def enqueue_frame(name, duration: nil, args: {})
|
35
48
|
queue.push([name, duration, args])
|
36
49
|
end
|
37
50
|
|
51
|
+
# Send any buffered frames to the EventLoop
|
38
52
|
def flush
|
39
|
-
while !queue.empty?
|
40
|
-
name, duration, args = queue.pop
|
41
|
-
send_frame(name: name, duration: duration, args: args)
|
42
|
-
end
|
53
|
+
consumer.enqueue(*queue.pop) while !queue.empty?
|
43
54
|
end
|
44
55
|
|
45
56
|
private
|
46
57
|
|
47
|
-
attr_reader :
|
58
|
+
attr_reader :consumer, :logger, :queue
|
48
59
|
end
|
49
60
|
end
|
50
61
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative '../utils/title_font'
|
2
|
+
|
1
3
|
require_relative 'settings'
|
2
4
|
|
3
5
|
module WhirledPeas
|
@@ -16,19 +18,23 @@ module WhirledPeas
|
|
16
18
|
private_constant :Element
|
17
19
|
|
18
20
|
class TextElement < Element
|
19
|
-
attr_reader :
|
21
|
+
attr_reader :lines
|
20
22
|
|
21
23
|
def initialize(name, settings)
|
22
24
|
super(name, TextSettings.merge(settings))
|
23
25
|
end
|
24
26
|
|
25
|
-
def
|
27
|
+
def lines=(val)
|
26
28
|
unless STRINGALBE_CLASSES.include?(val.class)
|
27
29
|
raise ArgmentError, "Unsupported type for TextElement: #{val.class}"
|
28
30
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
31
|
+
if settings.title_font
|
32
|
+
@lines = Utils::TitleFont.to_s(val.to_s, settings.title_font).split("\n")
|
33
|
+
else
|
34
|
+
@lines = [val.to_s]
|
35
|
+
end
|
36
|
+
@preferred_width = settings.width || @lines.first.length
|
37
|
+
@preferred_height = @lines.length
|
32
38
|
end
|
33
39
|
|
34
40
|
def inspect(indent='')
|
@@ -63,7 +69,7 @@ module WhirledPeas
|
|
63
69
|
|
64
70
|
def add_text(name=self.class.next_name, &block)
|
65
71
|
element = TextElement.new(name, settings)
|
66
|
-
element.
|
72
|
+
element.lines = yield nil, element.settings
|
67
73
|
children << element
|
68
74
|
end
|
69
75
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'json'
|
2
2
|
|
3
|
-
require_relative 'color'
|
3
|
+
require_relative '../utils/color'
|
4
|
+
require_relative '../utils/title_font'
|
4
5
|
|
5
6
|
module WhirledPeas
|
6
7
|
module UI
|
@@ -457,6 +458,12 @@ module WhirledPeas
|
|
457
458
|
class TextSettings < ElementSettings
|
458
459
|
include WidthSettings
|
459
460
|
include AlignSettings
|
461
|
+
|
462
|
+
attr_reader :title_font
|
463
|
+
|
464
|
+
def title_font=(font)
|
465
|
+
@title_font = Utils::TitleFont.validate!(font)
|
466
|
+
end
|
460
467
|
end
|
461
468
|
|
462
469
|
class ContainerSettings < ElementSettings
|
data/lib/whirled_peas/ui.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative '../template/element'
|
2
|
+
require_relative '../template/settings'
|
3
|
+
require_relative '../utils/ansi'
|
4
|
+
|
2
5
|
require_relative 'canvas'
|
3
|
-
require_relative 'settings'
|
4
6
|
|
5
7
|
module WhirledPeas
|
6
8
|
module UI
|
@@ -15,28 +17,30 @@ module WhirledPeas
|
|
15
17
|
end
|
16
18
|
|
17
19
|
def paint(&block)
|
18
|
-
|
20
|
+
text.lines.each.with_index do |line, index|
|
21
|
+
yield canvas.stroke(canvas.left, canvas.top + index, justified(line))
|
22
|
+
end
|
19
23
|
end
|
20
24
|
|
21
25
|
private
|
22
26
|
|
23
27
|
attr_reader :text, :canvas
|
24
28
|
|
25
|
-
def visible
|
26
|
-
if
|
27
|
-
|
29
|
+
def visible(line)
|
30
|
+
if line.length <= text.preferred_width
|
31
|
+
line
|
28
32
|
elsif text.settings.align == TextAlign::LEFT
|
29
|
-
|
33
|
+
line[0..text.preferred_width - 1]
|
30
34
|
elsif text.settings.align == TextAlign::CENTER
|
31
|
-
left_chop = (
|
32
|
-
right_chop =
|
33
|
-
|
35
|
+
left_chop = (line.length - text.preferred_width) / 2
|
36
|
+
right_chop = line.length - text.preferred_width - left_chop
|
37
|
+
line[left_chop..-right_chop - 1]
|
34
38
|
else
|
35
|
-
|
39
|
+
line[-text.preferred_width..-1]
|
36
40
|
end
|
37
41
|
end
|
38
42
|
|
39
|
-
def justified
|
43
|
+
def justified(line)
|
40
44
|
format_settings = [*text.settings.color, *text.settings.bg_color]
|
41
45
|
format_settings << Ansi::BOLD if text.settings.bold?
|
42
46
|
format_settings << Ansi::UNDERLINE if text.settings.underline?
|
@@ -45,13 +49,13 @@ module WhirledPeas
|
|
45
49
|
when TextAlign::LEFT
|
46
50
|
0
|
47
51
|
when TextAlign::CENTER
|
48
|
-
[0, (text.preferred_width -
|
52
|
+
[0, (text.preferred_width - line.length) / 2].max
|
49
53
|
when TextAlign::RIGHT
|
50
|
-
[0, text.preferred_width -
|
54
|
+
[0, text.preferred_width - line.length].max
|
51
55
|
end
|
52
|
-
rjust = [0, text.preferred_width -
|
56
|
+
rjust = [0, text.preferred_width - line.length - ljust].max
|
53
57
|
Ansi.format(JUSTIFICATION * ljust, [*text.settings.bg_color]) +
|
54
|
-
Ansi.format(visible, format_settings) +
|
58
|
+
Ansi.format(visible(line), format_settings) +
|
55
59
|
Ansi.format(JUSTIFICATION * rjust, [*text.settings.bg_color])
|
56
60
|
end
|
57
61
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'highline'
|
2
2
|
|
3
|
-
require_relative 'ansi'
|
3
|
+
require_relative '../utils/ansi'
|
4
|
+
|
4
5
|
require_relative 'painter'
|
5
6
|
|
6
7
|
module WhirledPeas
|
@@ -16,26 +17,13 @@ module WhirledPeas
|
|
16
17
|
|
17
18
|
def paint(template)
|
18
19
|
@template = template
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
def needs_refresh?
|
23
|
-
@refreshed_width != width || @refreshed_height != height
|
20
|
+
draw
|
24
21
|
end
|
25
22
|
|
26
23
|
def refresh
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
strokes << Ansi.cursor_pos(left: stroke.left, top: stroke.top)
|
31
|
-
strokes << stroke.chars
|
32
|
-
end
|
33
|
-
end
|
34
|
-
return unless @print_output
|
35
|
-
strokes.each(&method(:print))
|
36
|
-
STDOUT.flush
|
37
|
-
@refreshed_width = width
|
38
|
-
@refreshed_height = height
|
24
|
+
# No need to refresh if the screen dimensions have not changed
|
25
|
+
return if @refreshed_width == width || @refreshed_height == height
|
26
|
+
draw
|
39
27
|
end
|
40
28
|
|
41
29
|
def finalize
|
@@ -55,6 +43,21 @@ module WhirledPeas
|
|
55
43
|
private
|
56
44
|
|
57
45
|
attr_reader :cursor, :terminal, :width, :height
|
46
|
+
|
47
|
+
def draw
|
48
|
+
strokes = [Ansi.cursor_visible(false), Ansi.cursor_pos, Ansi.clear_down]
|
49
|
+
Painter.paint(@template, Canvas.new(0, 0, width, height)) do |stroke|
|
50
|
+
unless stroke.chars.nil?
|
51
|
+
strokes << Ansi.cursor_pos(left: stroke.left, top: stroke.top)
|
52
|
+
strokes << stroke.chars
|
53
|
+
end
|
54
|
+
end
|
55
|
+
return unless @print_output
|
56
|
+
strokes.each(&method(:print))
|
57
|
+
STDOUT.flush
|
58
|
+
@refreshed_width = width
|
59
|
+
@refreshed_height = height
|
60
|
+
end
|
58
61
|
end
|
59
62
|
end
|
60
63
|
end
|