whirled_peas 0.3.0 → 0.4.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/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
|