whirled_peas 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +1 -0
- data/README.md +23 -21
- data/lib/whirled_peas.rb +6 -12
- data/lib/whirled_peas/frame.rb +1 -4
- data/lib/whirled_peas/frame/event_loop.rb +1 -2
- data/lib/whirled_peas/frame/producer.rb +13 -26
- data/lib/whirled_peas/ui/ansi.rb +55 -106
- data/lib/whirled_peas/ui/canvas.rb +37 -4
- data/lib/whirled_peas/ui/color.rb +101 -0
- data/lib/whirled_peas/ui/element.rb +0 -1
- data/lib/whirled_peas/ui/painter.rb +2 -2
- data/lib/whirled_peas/ui/screen.rb +5 -7
- data/lib/whirled_peas/ui/settings.rb +11 -9
- data/lib/whirled_peas/version.rb +1 -1
- data/whirled_peas.gemspec +0 -1
- metadata +2 -17
- data/lib/whirled_peas/frame/consumer.rb +0 -66
- data/lib/whirled_peas/ui/stroke.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d3cb7aa7af0bf74f8818eb5796f310e3b65a73851b7cbab35834e14fe0262ac1
|
4
|
+
data.tar.gz: 6ae73d252d69d3730c1e1814d441800fca08177a4401abc3e4dcbe6edcd34548
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b0965214a99209e4830ed68da162cc83d249188e197d8b9fe11b61356f797e2172a648aef2b23f83f9ac45db40d9f4e7daee9471f67069e61998e5f24a8d0ba
|
7
|
+
data.tar.gz: e0d546f8a60606a918aadf1743848b9839703328bf14a05dcb67f680886ee2eef2c4ae542d89055daf822f74200f64a719e77f1223520266aa7d6703b18ccb03
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.2.0 - 2021-01-20
|
4
|
+
|
5
|
+
- [73eb326](https://github.com/tcollier/whirled_peas/tree/73eb326426f9814e91e3bc7a60dfd87be3d69f7e): Convert "primitive" data types to strings
|
6
|
+
- [f28b69d](https://github.com/tcollier/whirled_peas/tree/f28b69df8b6cfc973da2ebc0b8da29b278f23433): Give elements names
|
7
|
+
- [1ae1c929](https://github.com/tcollier/whirled_peas/tree/1ae1c929429c2f8520054d33a064c2b6d71955fe): Consistently format exceptions in logs
|
8
|
+
- [4c2114f](https://github.com/tcollier/whirled_peas/tree/4c2114fd360fd98c65e6e32f905a377f09b919ee): Fix bug with negative sleep times
|
9
|
+
- [627bf12](https://github.com/tcollier/whirled_peas/tree/627bf126dd7f9c845f65105e0826d14a35a0a953): Don't reraise pipe error
|
10
|
+
|
3
11
|
## v0.1.1 - 2021-01-20
|
4
12
|
|
5
13
|
- [3852c8c](https://github.com/tcollier/whirled_peas/tree/3852c8c700c2e8fb92e65bbca1c99be74304c6d0): Improve error handling
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[![Build Status](https://travis-ci.com/tcollier/whirled_peas.svg?branch=main)](https://travis-ci.com/tcollier/whirled_peas)
|
2
|
+
|
1
3
|
# WhirledPeas
|
2
4
|
|
3
5
|
Visualize your code's execution with Whirled Peas!
|
@@ -37,7 +39,7 @@ end
|
|
37
39
|
|
38
40
|
class Driver
|
39
41
|
def start(producer)
|
40
|
-
producer.
|
42
|
+
producer.send_frame('starting', args: { 'name' => 'World' })
|
41
43
|
# ...
|
42
44
|
end
|
43
45
|
end
|
@@ -70,9 +72,8 @@ The producer provides a single method
|
|
70
72
|
#
|
71
73
|
# @param name [String] application defined name for the frame. The template factory will be provided this name
|
72
74
|
# @param duration [Number] time in seconds this frame should be displayed for (defaults to 1 frame)
|
73
|
-
# @param args [Hash] key value pairs to send as arguments to the template factory
|
74
|
-
|
75
|
-
def send(name, duration:, args:)
|
75
|
+
# @param args [Hash] key value pairs to send as arguments to the template factory
|
76
|
+
def send_frame(name, duration:, args:)
|
76
77
|
# implementation
|
77
78
|
end
|
78
79
|
```
|
@@ -85,25 +86,25 @@ Simple application that loads a set of numbers and looks for a pair that adds up
|
|
85
86
|
class Driver
|
86
87
|
def start(producer)
|
87
88
|
numbers = File.readlines('/path/to/numbers.txt').map(&:to_i)
|
88
|
-
producer.
|
89
|
+
producer.send_frame('load-numbers', duration: 3, args: { numbers: numbers })
|
89
90
|
numbers.sort!
|
90
|
-
producer.
|
91
|
+
producer.send_frame('sort-numbers', duration: 3, args: { numbers: numbers })
|
91
92
|
low = 0
|
92
93
|
high = numbers.length - 1
|
93
94
|
while low < high
|
94
95
|
sum = numbers[low] + numbers[high]
|
95
96
|
if sum == 1000
|
96
|
-
producer.
|
97
|
+
producer.send_frame('found-pair', duration: 5, args: { low: low, high: high, sum: sum })
|
97
98
|
return
|
98
99
|
elsif sum < 1000
|
99
|
-
producer.
|
100
|
+
producer.send_frame('too-low', args: { low: low, high: high, sum: sum })
|
100
101
|
low += 1
|
101
102
|
else
|
102
|
-
producer.
|
103
|
+
producer.send_frame('too-high', args: { low: low, high: high, sum: sum })
|
103
104
|
high -= 1
|
104
105
|
end
|
105
106
|
end
|
106
|
-
producer.
|
107
|
+
producer.send_frame('no-solution', duration: 5)
|
107
108
|
end
|
108
109
|
end
|
109
110
|
```
|
@@ -116,7 +117,7 @@ To render the frame events sent by the driver, the application requires a templa
|
|
116
117
|
|
117
118
|
A template is created with `WhirledPeas.template`, which yields a `Template` object and `TemplateSettings`. This template object is a `ComposableElement`, which allows for attaching child elements and setting layout options. `GridElement` and `BoxElement` are two other composable elements and `TextElement` is a simple element that can hold a text/number value and has layout options, but cannot have any child elements.
|
118
119
|
|
119
|
-
A `ComposableElement` provides the following methods to add child elements
|
120
|
+
A `ComposableElement` provides the following methods to add child elements, each of these takes an optional string argument that is set as the name of the element (which can be useful when debugging).
|
120
121
|
|
121
122
|
- `add_box` - yields a `ComposableElement` and a `BoxSettings`, which will be added to the parent's children
|
122
123
|
- `add_grid` - yields a `ComposableElement` and a `GridSettings`, which will be added to the parent's children
|
@@ -202,15 +203,16 @@ The available settigs are
|
|
202
203
|
|
203
204
|
Margin and padding settings allow for setting the spacing on each of the 4 sides of the element independently. The set these values, use
|
204
205
|
|
206
|
+
- `clear_margin` - sets all margin values to 0
|
205
207
|
- `set_margin(left:, top:, right:, bottom:)`
|
208
|
+
- `clear_padding` - sets all margin values to 0
|
206
209
|
- `set_padding(left:, top:, right:, bottom:)`
|
207
210
|
|
208
|
-
Any argument value not provided will result in that value being 0.
|
209
|
-
|
210
211
|
##### Border
|
211
212
|
|
212
213
|
The border settings consist of 6 boolean values (border are either width 1 or not shown), the 4 obvious values (`left`, `top`, `right`, and `bottom`) along with 2 other values for inner borders (`inner_horiz` and `inner_vert`) in a grid. A border also has a foreground color (defaults to `:white`) and a style. The background color is determined by the `bg_color` of the element. Border values can be set with
|
213
214
|
|
215
|
+
- `clear_border` - sets all border positions to `false`
|
214
216
|
- `set_border(left:, top:, right:, bottom:, inner_horiz:, inner_vert:, color:, style:)`
|
215
217
|
|
216
218
|
Available border styles are
|
@@ -309,7 +311,7 @@ class TemplateFactory
|
|
309
311
|
def build(frame, args)
|
310
312
|
set_state(frame, args)
|
311
313
|
WhirledPeas.template do |t|
|
312
|
-
t.add_box(&method(:body))
|
314
|
+
t.add_box('Body', &method(:body))
|
313
315
|
end
|
314
316
|
end
|
315
317
|
|
@@ -317,10 +319,10 @@ class TemplateFactory
|
|
317
319
|
|
318
320
|
def set_state(frame, args)
|
319
321
|
@frame = frame
|
320
|
-
@numbers = args.key?(
|
321
|
-
@sum = args[
|
322
|
-
@low = args[
|
323
|
-
@high = args[
|
322
|
+
@numbers = args.key?(:numbers) ? args[:numbers] || []
|
323
|
+
@sum = args[:sum] if args.key?(:sum)
|
324
|
+
@low = args[:low] if args.key?(:low)
|
325
|
+
@high = args[:high] if args.key?(:high)
|
324
326
|
end
|
325
327
|
|
326
328
|
def title(_elem, settings)
|
@@ -347,9 +349,9 @@ class TemplateFactory
|
|
347
349
|
settings.flow = :l2r
|
348
350
|
settings.auto_margin = true
|
349
351
|
|
350
|
-
elem.add_box(&method(:title))
|
351
|
-
elem.add_box(&method(:sum))
|
352
|
-
elem.add_grid(&method(:number_grid))
|
352
|
+
elem.add_box('Title', &method(:title))
|
353
|
+
elem.add_box('Sum', &method(:sum))
|
354
|
+
elem.add_grid('NumberGrid', &method(:number_grid))
|
353
355
|
end
|
354
356
|
end
|
355
357
|
```
|
data/lib/whirled_peas.rb
CHANGED
@@ -7,13 +7,11 @@ require 'whirled_peas/version'
|
|
7
7
|
module WhirledPeas
|
8
8
|
class Error < StandardError; end
|
9
9
|
|
10
|
-
DEFAULT_HOST = 'localhost'
|
11
|
-
DEFAULT_PORT = 8765
|
12
10
|
DEFAULT_REFRESH_RATE = 30
|
13
11
|
|
14
12
|
LOGGER_ID = 'MAIN'
|
15
13
|
|
16
|
-
def self.start(driver, template_factory, log_level: Logger::INFO, refresh_rate: DEFAULT_REFRESH_RATE
|
14
|
+
def self.start(driver, template_factory, log_level: Logger::INFO, refresh_rate: DEFAULT_REFRESH_RATE)
|
17
15
|
logger = Logger.new(File.open('whirled_peas.log', 'a'))
|
18
16
|
logger.level = log_level
|
19
17
|
logger.formatter = proc do |severity, datetime, progname, msg|
|
@@ -23,27 +21,23 @@ module WhirledPeas
|
|
23
21
|
"[#{severity}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%L')} (#{progname}) - #{msg}\n"
|
24
22
|
end
|
25
23
|
|
26
|
-
|
27
|
-
|
24
|
+
event_loop = Frame::EventLoop.new(template_factory, refresh_rate, logger)
|
25
|
+
event_loop_thread = Thread.new do
|
28
26
|
Thread.current.report_on_exception = false
|
29
|
-
|
27
|
+
event_loop.start
|
30
28
|
end
|
31
29
|
|
32
|
-
Frame::Producer.
|
30
|
+
Frame::Producer.produce(event_loop: event_loop, logger: logger) do |producer|
|
33
31
|
begin
|
34
32
|
driver.start(producer)
|
35
|
-
producer.stop
|
36
|
-
rescue Errno::EPIPE
|
37
|
-
logger.error(LOGGER_ID) { 'Producer cannot connect to consumer, exiting...' }
|
38
33
|
rescue => e
|
39
34
|
logger.warn(LOGGER_ID) { 'Driver exited with error, terminating producer...' }
|
40
35
|
logger.error(LOGGER_ID) { e }
|
41
|
-
producer.terminate
|
42
36
|
raise
|
43
37
|
end
|
44
38
|
end
|
45
39
|
|
46
|
-
|
40
|
+
event_loop_thread.join
|
47
41
|
end
|
48
42
|
|
49
43
|
def self.template(&block)
|
data/lib/whirled_peas/frame.rb
CHANGED
@@ -1,13 +1,10 @@
|
|
1
|
-
require_relative 'frame/
|
1
|
+
require_relative 'frame/event_loop'
|
2
2
|
require_relative 'frame/producer'
|
3
3
|
|
4
4
|
module WhirledPeas
|
5
5
|
module Frame
|
6
6
|
TERMINATE = '__term__'
|
7
7
|
EOF = '__EOF__'
|
8
|
-
|
9
|
-
DEFAULT_ADDRESS = 'localhost'
|
10
|
-
DEFAULT_PORT = 8765
|
11
8
|
end
|
12
9
|
|
13
10
|
private_constant :Frame
|
@@ -8,7 +8,7 @@ module WhirledPeas
|
|
8
8
|
@logger = logger
|
9
9
|
end
|
10
10
|
|
11
|
-
def enqueue(name
|
11
|
+
def enqueue(name:, duration:, args:)
|
12
12
|
queue.push([name, duration, args])
|
13
13
|
end
|
14
14
|
|
@@ -55,6 +55,5 @@ module WhirledPeas
|
|
55
55
|
|
56
56
|
attr_reader :template_factory, :queue, :refresh_rate, :logger
|
57
57
|
end
|
58
|
-
private_constant :EventLoop
|
59
58
|
end
|
60
59
|
end
|
@@ -6,58 +6,45 @@ module WhirledPeas
|
|
6
6
|
class Producer
|
7
7
|
LOGGER_ID = 'PRODUCER'
|
8
8
|
|
9
|
-
def self.
|
10
|
-
|
11
|
-
|
12
|
-
logger.info(LOGGER_ID) { "Connected to #{host}:#{port}" }
|
13
|
-
producer = new(client, logger)
|
9
|
+
def self.produce(event_loop:, logger: NullLogger.new)
|
10
|
+
producer = new(event_loop, logger)
|
11
|
+
logger.info(LOGGER_ID) { 'Starting' }
|
14
12
|
yield producer
|
13
|
+
logger.info(LOGGER_ID) { 'Done with yield' }
|
14
|
+
producer.send_frame(Frame::EOF)
|
15
15
|
logger.info(LOGGER_ID) { 'Exited normally' }
|
16
16
|
rescue => e
|
17
|
-
producer.
|
17
|
+
producer.send_frame(Frame::TERMINATE)
|
18
18
|
logger.warn(LOGGER_ID) { 'Exited with error' }
|
19
19
|
logger.error(LOGGER_ID) { e }
|
20
20
|
raise
|
21
|
-
ensure
|
22
|
-
if client
|
23
|
-
logger.info(LOGGER_ID) { 'Closing connection'}
|
24
|
-
client.close
|
25
|
-
end
|
26
21
|
end
|
27
22
|
|
28
|
-
def initialize(
|
29
|
-
@
|
23
|
+
def initialize(event_loop, logger=NullLogger.new)
|
24
|
+
@event_loop = event_loop
|
30
25
|
@logger = logger
|
31
26
|
@queue = Queue.new
|
32
27
|
end
|
33
28
|
|
34
|
-
def
|
35
|
-
|
29
|
+
def send_frame(name, duration: nil, args: {})
|
30
|
+
event_loop.enqueue(name: name, duration: duration, args: args)
|
36
31
|
logger.debug(LOGGER_ID) { "Sending frame: #{name}" }
|
37
32
|
end
|
38
33
|
|
39
|
-
def
|
34
|
+
def enqueue_frame(name, duration: nil, args: {})
|
40
35
|
queue.push([name, duration, args])
|
41
36
|
end
|
42
37
|
|
43
38
|
def flush
|
44
39
|
while !queue.empty?
|
45
40
|
name, duration, args = queue.pop
|
46
|
-
|
41
|
+
send_frame(name: name, duration: duration, args: args)
|
47
42
|
end
|
48
43
|
end
|
49
44
|
|
50
|
-
def stop
|
51
|
-
send(Frame::EOF)
|
52
|
-
end
|
53
|
-
|
54
|
-
def terminate
|
55
|
-
send(Frame::TERMINATE)
|
56
|
-
end
|
57
|
-
|
58
45
|
private
|
59
46
|
|
60
|
-
attr_reader :
|
47
|
+
attr_reader :event_loop, :logger
|
61
48
|
end
|
62
49
|
end
|
63
50
|
end
|
data/lib/whirled_peas/ui/ansi.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
module WhirledPeas
|
2
2
|
module UI
|
3
|
-
|
4
|
-
|
3
|
+
# Helper module for working with ANSI escape codes. The most useful ANSI escape codes
|
4
|
+
# relate to text formatting.
|
5
|
+
#
|
6
|
+
# @see https://en.wikipedia.org/wiki/ANSI_escape_code
|
5
7
|
module Ansi
|
8
|
+
ESC = "\033"
|
9
|
+
|
10
|
+
# Text formatting constants
|
6
11
|
BOLD = 1
|
7
12
|
UNDERLINE = 4
|
8
13
|
|
14
|
+
# Text and background color constants
|
9
15
|
BLACK = 30
|
10
16
|
RED = 31
|
11
17
|
GREEN = 32
|
@@ -16,8 +22,26 @@ module WhirledPeas
|
|
16
22
|
WHITE = 37
|
17
23
|
|
18
24
|
END_FORMATTING = 0
|
25
|
+
private_constant :END_FORMATTING
|
19
26
|
|
20
27
|
class << self
|
28
|
+
def cursor_pos(top: 0, left: 0)
|
29
|
+
"#{ESC}[#{top + 1};#{left + 1}H"
|
30
|
+
end
|
31
|
+
|
32
|
+
def cursor_visible(visible)
|
33
|
+
visible ? "#{ESC}[?25h" : "#{ESC}[?25l"
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear_down
|
37
|
+
"#{ESC}[J"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Format the string with the ANSI escapes codes for the given integer codes
|
41
|
+
#
|
42
|
+
# @param str [String] the string to format
|
43
|
+
# @param codes [Array<Integer>] the integer part of the ANSI escape code (see
|
44
|
+
# constants in this module for codes and meanings)
|
21
45
|
def format(str, codes)
|
22
46
|
if str.empty? || codes.length == 0
|
23
47
|
str
|
@@ -31,124 +55,49 @@ module WhirledPeas
|
|
31
55
|
esc_seq(END_FORMATTING)
|
32
56
|
end
|
33
57
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
width
|
39
|
-
end
|
40
|
-
|
41
|
-
def close_formatting(line)
|
42
|
-
codes = line.scan(DEBUG_COLOR ? /<(\d+)>/ : /\033\[(\d+)m/)
|
58
|
+
# If the string has unclosed formatting, add the end formatting characters to
|
59
|
+
# the end of the string
|
60
|
+
def close_formatting(str)
|
61
|
+
codes = str.scan(/#{ESC}\[(\d+)m/)
|
43
62
|
if codes.length > 0 && codes.last[0] != END_FORMATTING.to_s
|
44
|
-
"#{
|
63
|
+
"#{str}#{esc_seq(END_FORMATTING)}"
|
45
64
|
else
|
46
|
-
|
65
|
+
str
|
47
66
|
end
|
48
67
|
end
|
49
68
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
69
|
+
# Return a substring of the input string that preservse the formatting
|
70
|
+
#
|
71
|
+
# @param str [String] the (possibly formatted) string
|
72
|
+
# @param first_visible_character [Integer] the index of the first character to
|
73
|
+
# include in the substring (ignoring all hidden formatting characters)
|
74
|
+
# @param num_visible_chars [Integer] the maximum number of visible characters to
|
75
|
+
# include in the substring (ignoring all hidden formatting characters)
|
76
|
+
def substring(str, first_visible_character, num_visible_chars)
|
77
|
+
substr = ''
|
78
|
+
is_visible = true
|
79
|
+
visible_index = 0
|
80
|
+
substr_visible_len = 0
|
55
81
|
str.chars.each do |char|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
82
|
+
in_substring = (visible_index >= first_visible_character)
|
83
|
+
is_visible = false if is_visible && char == ESC
|
84
|
+
visible_index += 1 if is_visible
|
85
|
+
if !is_visible || in_substring
|
86
|
+
substr += char
|
87
|
+
substr_visible_len += 1 if is_visible
|
88
|
+
end
|
89
|
+
is_visible = true if !is_visible && char == 'm'
|
90
|
+
break if substr_visible_len == num_visible_chars
|
61
91
|
end
|
62
|
-
close_formatting(
|
92
|
+
close_formatting(substr)
|
63
93
|
end
|
64
94
|
|
65
95
|
private
|
66
96
|
|
67
97
|
def esc_seq(code)
|
68
|
-
|
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
|
98
|
+
"#{ESC}[#{code}m"
|
96
99
|
end
|
97
100
|
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
101
|
end
|
153
102
|
end
|
154
103
|
end
|
@@ -1,8 +1,39 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative 'ansi'
|
2
2
|
|
3
3
|
module WhirledPeas
|
4
4
|
module UI
|
5
|
+
# Canvas represent the area of the screen a painter can paint on.
|
5
6
|
class Canvas
|
7
|
+
# A Stroke is a single line, formatted string of characters that is painted at
|
8
|
+
# a given position on a Canvas. This class is not meant to be instantiated
|
9
|
+
# directly. Instead, use Canvas#stroke to create a new Stroke.
|
10
|
+
class Stroke
|
11
|
+
attr_reader :left, :top, :chars
|
12
|
+
|
13
|
+
def initialize(left, top, chars)
|
14
|
+
@left = left
|
15
|
+
@top = top
|
16
|
+
@chars = chars
|
17
|
+
end
|
18
|
+
|
19
|
+
def hash
|
20
|
+
[left, top, chars].hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
other.is_a?(self.class) && self.hash == other.hash
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
"Stroke(left=#{left}, top=#{top}, chars=#{chars})"
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :eq?, :==
|
32
|
+
end
|
33
|
+
private_constant :Stroke
|
34
|
+
|
35
|
+
EMPTY_STROKE = Stroke.new(nil, nil, nil)
|
36
|
+
|
6
37
|
attr_reader :left, :top, :width, :height
|
7
38
|
|
8
39
|
def initialize(left, top, width, height)
|
@@ -12,18 +43,20 @@ module WhirledPeas
|
|
12
43
|
@height = height
|
13
44
|
end
|
14
45
|
|
46
|
+
# Return a new Stroke instance, verifying only characters within the canvas
|
47
|
+
# are included in the stroke.
|
15
48
|
def stroke(left, top, chars)
|
16
49
|
if left >= self.left + self.width || left + chars.length <= self.left
|
17
|
-
|
50
|
+
EMPTY_STROKE
|
18
51
|
elsif top < self.top || top >= self.top + self.height
|
19
|
-
|
52
|
+
EMPTY_STROKE
|
20
53
|
else
|
21
54
|
if left < self.left
|
22
55
|
chars = chars[self.left - left..-1]
|
23
56
|
left = self.left
|
24
57
|
end
|
25
58
|
num_chars = [self.left + self.width, left + chars.length].min - left
|
26
|
-
Stroke.new(left, top, Ansi.
|
59
|
+
Stroke.new(left, top, Ansi.substring(chars, 0, num_chars))
|
27
60
|
end
|
28
61
|
end
|
29
62
|
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative 'ansi'
|
2
|
+
|
3
|
+
module WhirledPeas
|
4
|
+
module UI
|
5
|
+
# An abstract class that encapsulates colors for a specific use case
|
6
|
+
class Color
|
7
|
+
# The ANSI codes for bright colors are offset by this much from their
|
8
|
+
# standard versions
|
9
|
+
BRIGHT_OFFSET = 60
|
10
|
+
private_constant :BRIGHT_OFFSET
|
11
|
+
|
12
|
+
# Validate the `color` argument is either (1) nil, (2) a valid Color constant in
|
13
|
+
# this class or (3) a symbol that maps to valid Color constant. E.g. if there
|
14
|
+
# is a RED constant in an implementing class, then :red or :bright_red are
|
15
|
+
# valid values for `color`
|
16
|
+
#
|
17
|
+
# @param color [Color|Symbol]
|
18
|
+
# @return [Color|Symbol] the value passed in if valid, otherwise an ArgumentError
|
19
|
+
# is raised.
|
20
|
+
def self.validate!(color)
|
21
|
+
return unless color
|
22
|
+
if color.is_a?(Symbol)
|
23
|
+
error_message = "Unsupported #{self.name.split('::').last}: #{color.inspect}"
|
24
|
+
match = color.to_s.match(/^(bright_)?(\w+)$/)
|
25
|
+
begin
|
26
|
+
color = self.const_get(match[2].upcase)
|
27
|
+
raise ArgumentError, error_message unless color.is_a?(Color)
|
28
|
+
if match[1]
|
29
|
+
raise ArgumentError, error_message if color.bright?
|
30
|
+
color.bright
|
31
|
+
else
|
32
|
+
color
|
33
|
+
end
|
34
|
+
rescue NameError
|
35
|
+
raise ArgumentError, error_message
|
36
|
+
end
|
37
|
+
else
|
38
|
+
color
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(code, bright=false)
|
43
|
+
@code = code
|
44
|
+
@bright = bright
|
45
|
+
end
|
46
|
+
|
47
|
+
def bright?
|
48
|
+
@bright
|
49
|
+
end
|
50
|
+
|
51
|
+
def bright
|
52
|
+
bright? ? self : self.class.new(@code + BRIGHT_OFFSET, true)
|
53
|
+
end
|
54
|
+
|
55
|
+
def hash
|
56
|
+
[@code, @bright].hash
|
57
|
+
end
|
58
|
+
|
59
|
+
def ==(other)
|
60
|
+
other.is_a?(self.class) && self.hash == other.hash
|
61
|
+
end
|
62
|
+
alias_method :eq?, :==
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
@code.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
"#{self.class.name.split('::').last}(code=#{@code}, bright=#{@bright})"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
private_constant :Color
|
73
|
+
|
74
|
+
class BgColor < Color
|
75
|
+
BG_OFFSET = 10
|
76
|
+
private_constant :BG_OFFSET
|
77
|
+
|
78
|
+
BLACK = new(Ansi::BLACK + BG_OFFSET)
|
79
|
+
RED = new(Ansi::RED + BG_OFFSET)
|
80
|
+
GREEN = new(Ansi::GREEN + BG_OFFSET)
|
81
|
+
YELLOW = new(Ansi::YELLOW + BG_OFFSET)
|
82
|
+
BLUE = new(Ansi::BLUE + BG_OFFSET)
|
83
|
+
MAGENTA = new(Ansi::MAGENTA + BG_OFFSET)
|
84
|
+
CYAN = new(Ansi::CYAN + BG_OFFSET)
|
85
|
+
WHITE = new(Ansi::WHITE + BG_OFFSET)
|
86
|
+
GRAY = BLACK.bright
|
87
|
+
end
|
88
|
+
|
89
|
+
class TextColor < Color
|
90
|
+
BLACK = new(Ansi::BLACK)
|
91
|
+
RED = new(Ansi::RED)
|
92
|
+
GREEN = new(Ansi::GREEN)
|
93
|
+
YELLOW = new(Ansi::YELLOW)
|
94
|
+
BLUE = new(Ansi::BLUE)
|
95
|
+
MAGENTA = new(Ansi::MAGENTA)
|
96
|
+
CYAN = new(Ansi::CYAN)
|
97
|
+
WHITE = new(Ansi::WHITE)
|
98
|
+
GRAY = BLACK.bright
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -38,8 +38,8 @@ module WhirledPeas
|
|
38
38
|
|
39
39
|
def justified
|
40
40
|
format_settings = [*text.settings.color, *text.settings.bg_color]
|
41
|
-
format_settings <<
|
42
|
-
format_settings <<
|
41
|
+
format_settings << Ansi::BOLD if text.settings.bold?
|
42
|
+
format_settings << Ansi::UNDERLINE if text.settings.underline?
|
43
43
|
|
44
44
|
ljust = case text.settings.align
|
45
45
|
when TextAlign::LEFT
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'highline'
|
2
|
-
require 'tty-cursor'
|
3
2
|
|
4
3
|
require_relative 'ansi'
|
5
4
|
require_relative 'painter'
|
@@ -10,7 +9,6 @@ module WhirledPeas
|
|
10
9
|
def initialize(print_output=true)
|
11
10
|
@print_output = print_output
|
12
11
|
@terminal = HighLine.new.terminal
|
13
|
-
@cursor = TTY::Cursor
|
14
12
|
@strokes = []
|
15
13
|
refresh_size!
|
16
14
|
Signal.trap('SIGWINCH', proc { self.refresh_size! })
|
@@ -26,10 +24,10 @@ module WhirledPeas
|
|
26
24
|
end
|
27
25
|
|
28
26
|
def refresh
|
29
|
-
strokes = [
|
27
|
+
strokes = [Ansi.cursor_visible(false), Ansi.cursor_pos, Ansi.clear_down]
|
30
28
|
Painter.paint(@template, Canvas.new(0, 0, width, height)) do |stroke|
|
31
29
|
unless stroke.chars.nil?
|
32
|
-
strokes <<
|
30
|
+
strokes << Ansi.cursor_pos(left: stroke.left, top: stroke.top)
|
33
31
|
strokes << stroke.chars
|
34
32
|
end
|
35
33
|
end
|
@@ -42,9 +40,9 @@ module WhirledPeas
|
|
42
40
|
|
43
41
|
def finalize
|
44
42
|
return unless @print_output
|
45
|
-
print
|
46
|
-
print
|
47
|
-
print
|
43
|
+
print Ansi.clear
|
44
|
+
print Ansi.cursor_pos(top: height - 1)
|
45
|
+
print Ansi.cursor_visible(true)
|
48
46
|
STDOUT.flush
|
49
47
|
end
|
50
48
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'json'
|
2
2
|
|
3
|
+
require_relative 'color'
|
4
|
+
|
3
5
|
module WhirledPeas
|
4
6
|
module UI
|
5
7
|
module TextAlign
|
@@ -305,11 +307,11 @@ module WhirledPeas
|
|
305
307
|
end
|
306
308
|
private_constant :ElementSettings
|
307
309
|
|
308
|
-
module
|
310
|
+
module WidthSettings
|
309
311
|
attr_accessor :width
|
310
312
|
end
|
311
313
|
|
312
|
-
module
|
314
|
+
module AlignSettings
|
313
315
|
def align
|
314
316
|
@_align || TextAlign::LEFT
|
315
317
|
end
|
@@ -322,7 +324,7 @@ module WhirledPeas
|
|
322
324
|
merged = super
|
323
325
|
merged._align = if @_align
|
324
326
|
@_align
|
325
|
-
elsif parent.is_a?(
|
327
|
+
elsif parent.is_a?(AlignSettings)
|
326
328
|
parent._align
|
327
329
|
end
|
328
330
|
merged
|
@@ -453,8 +455,8 @@ module WhirledPeas
|
|
453
455
|
end
|
454
456
|
|
455
457
|
class TextSettings < ElementSettings
|
456
|
-
include
|
457
|
-
include
|
458
|
+
include WidthSettings
|
459
|
+
include AlignSettings
|
458
460
|
end
|
459
461
|
|
460
462
|
class ContainerSettings < ElementSettings
|
@@ -464,8 +466,8 @@ module WhirledPeas
|
|
464
466
|
end
|
465
467
|
|
466
468
|
class BoxSettings < ContainerSettings
|
467
|
-
include
|
468
|
-
include
|
469
|
+
include WidthSettings
|
470
|
+
include AlignSettings
|
469
471
|
|
470
472
|
def flow=(flow)
|
471
473
|
@_flow = DisplayFlow.validate!(flow)
|
@@ -507,8 +509,8 @@ module WhirledPeas
|
|
507
509
|
end
|
508
510
|
|
509
511
|
class GridSettings < ContainerSettings
|
510
|
-
include
|
511
|
-
include
|
512
|
+
include WidthSettings
|
513
|
+
include AlignSettings
|
512
514
|
|
513
515
|
attr_accessor :num_cols
|
514
516
|
attr_writer :transpose
|
data/lib/whirled_peas/version.rb
CHANGED
data/whirled_peas.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: whirled_peas
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Collier
|
@@ -38,20 +38,6 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '2.5'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: tty-cursor
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0.7'
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0.7'
|
55
41
|
description:
|
56
42
|
email:
|
57
43
|
- tcollier@gmail.com
|
@@ -72,18 +58,17 @@ files:
|
|
72
58
|
- bin/setup
|
73
59
|
- lib/whirled_peas.rb
|
74
60
|
- lib/whirled_peas/frame.rb
|
75
|
-
- lib/whirled_peas/frame/consumer.rb
|
76
61
|
- lib/whirled_peas/frame/event_loop.rb
|
77
62
|
- lib/whirled_peas/frame/producer.rb
|
78
63
|
- lib/whirled_peas/null_logger.rb
|
79
64
|
- lib/whirled_peas/ui.rb
|
80
65
|
- lib/whirled_peas/ui/ansi.rb
|
81
66
|
- lib/whirled_peas/ui/canvas.rb
|
67
|
+
- lib/whirled_peas/ui/color.rb
|
82
68
|
- lib/whirled_peas/ui/element.rb
|
83
69
|
- lib/whirled_peas/ui/painter.rb
|
84
70
|
- lib/whirled_peas/ui/screen.rb
|
85
71
|
- lib/whirled_peas/ui/settings.rb
|
86
|
-
- lib/whirled_peas/ui/stroke.rb
|
87
72
|
- lib/whirled_peas/version.rb
|
88
73
|
- sandbox/auto.rb
|
89
74
|
- sandbox/box.rb
|
@@ -1,66 +0,0 @@
|
|
1
|
-
require 'socket'
|
2
|
-
require 'json'
|
3
|
-
|
4
|
-
require_relative 'event_loop'
|
5
|
-
|
6
|
-
module WhirledPeas
|
7
|
-
module Frame
|
8
|
-
class Consumer
|
9
|
-
LOGGER_ID = 'CONSUMER'
|
10
|
-
|
11
|
-
def initialize(template_factory, refresh_rate, logger=NullLogger.new)
|
12
|
-
@event_loop = EventLoop.new(template_factory, refresh_rate, logger)
|
13
|
-
@logger = logger
|
14
|
-
@running = false
|
15
|
-
@mutex = Mutex.new
|
16
|
-
end
|
17
|
-
|
18
|
-
def start(host:, port:)
|
19
|
-
mutex.synchronize { @running = true }
|
20
|
-
loop_thread = Thread.new do
|
21
|
-
Thread.current.report_on_exception = false
|
22
|
-
event_loop.start
|
23
|
-
end
|
24
|
-
socket = TCPSocket.new(host, port)
|
25
|
-
logger.info(LOGGER_ID) { "Connected to #{host}:#{port}" }
|
26
|
-
while @running && event_loop.running?
|
27
|
-
line = socket.gets
|
28
|
-
if line.nil?
|
29
|
-
sleep(0.001)
|
30
|
-
next
|
31
|
-
end
|
32
|
-
args = JSON.parse(line)
|
33
|
-
name = args.delete('name')
|
34
|
-
if [Frame::EOF, Frame::TERMINATE].include?(name)
|
35
|
-
logger.info(LOGGER_ID) { "Received #{name} event, stopping..." }
|
36
|
-
event_loop.stop if name == Frame::TERMINATE
|
37
|
-
@running = false
|
38
|
-
else
|
39
|
-
duration = args.delete('duration')
|
40
|
-
event_loop.enqueue(name, duration, args)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
logger.info(LOGGER_ID) { 'Exited normally' }
|
44
|
-
logger.info(LOGGER_ID) { 'Waiting for loop thread to exit' }
|
45
|
-
loop_thread.join
|
46
|
-
rescue => e
|
47
|
-
event_loop.stop if event_loop.running?
|
48
|
-
logger.warn(LOGGER_ID) { 'Exited with error' }
|
49
|
-
logger.error(LOGGER_ID) { e }
|
50
|
-
raise
|
51
|
-
ensure
|
52
|
-
logger.info(LOGGER_ID) { 'Closing socket' }
|
53
|
-
socket.close if socket
|
54
|
-
end
|
55
|
-
|
56
|
-
def stop
|
57
|
-
logger.info(LOGGER_ID) { 'Stopping...' }
|
58
|
-
mutex.synchronize { @running = false }
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
attr_reader :event_loop, :logger, :mutex
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
module WhirledPeas
|
2
|
-
module UI
|
3
|
-
class Stroke
|
4
|
-
EMPTY = Stroke.new
|
5
|
-
|
6
|
-
attr_reader :left, :top, :chars
|
7
|
-
|
8
|
-
def initialize(left, top, chars)
|
9
|
-
@left = left
|
10
|
-
@top = top
|
11
|
-
@chars = chars
|
12
|
-
end
|
13
|
-
|
14
|
-
def hash
|
15
|
-
[left, top, chars].hash
|
16
|
-
end
|
17
|
-
|
18
|
-
def ==(other)
|
19
|
-
other.is_a?(self.class) && self.hash == other.hash
|
20
|
-
end
|
21
|
-
|
22
|
-
def inspect
|
23
|
-
"Stroke(left=#{left}, top=#{top}, chars=#{chars})"
|
24
|
-
end
|
25
|
-
|
26
|
-
alias_method :eql?, :==
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|