whirled_peas 0.1.0 → 0.4.1
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 +29 -0
- data/Gemfile +1 -0
- data/README.md +213 -48
- data/bin/title_fonts +6 -0
- data/exe/whirled_peas +7 -0
- data/lib/whirled_peas.rb +12 -30
- data/lib/whirled_peas/command_line.rb +270 -0
- data/lib/whirled_peas/config.rb +21 -0
- data/lib/whirled_peas/errors.rb +5 -0
- data/lib/whirled_peas/frame.rb +0 -7
- data/lib/whirled_peas/frame/event_loop.rb +91 -0
- data/lib/whirled_peas/frame/print_consumer.rb +33 -0
- data/lib/whirled_peas/frame/producer.rb +35 -31
- data/lib/whirled_peas/template.rb +5 -0
- data/lib/whirled_peas/template/element.rb +230 -0
- data/lib/whirled_peas/{ui → template}/settings.rb +28 -10
- data/lib/whirled_peas/ui.rb +1 -3
- data/lib/whirled_peas/ui/canvas.rb +37 -4
- data/lib/whirled_peas/ui/painter.rb +22 -18
- data/lib/whirled_peas/ui/screen.rb +24 -23
- data/lib/whirled_peas/utils.rb +5 -0
- data/lib/whirled_peas/utils/ansi.rb +103 -0
- data/lib/whirled_peas/{ui/ansi.rb → utils/color.rb} +23 -76
- data/lib/whirled_peas/utils/title_font.rb +75 -0
- data/lib/whirled_peas/version.rb +1 -1
- data/whirled_peas.gemspec +4 -2
- metadata +22 -18
- data/lib/whirled_peas/frame/consumer.rb +0 -61
- data/lib/whirled_peas/frame/loop.rb +0 -56
- data/lib/whirled_peas/ui/element.rb +0 -199
- data/lib/whirled_peas/ui/stroke.rb +0 -29
- 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,75 @@
|
|
1
|
+
module WhirledPeas
|
2
|
+
module Utils
|
3
|
+
module TitleFont
|
4
|
+
PREFERRED_DEFAULT_FONT = :ansishadow
|
5
|
+
FONT_CACHE = File.join(ENV['HOME'] || '.', '.whirled_peas.fonts')
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def validate!(font_key)
|
9
|
+
return unless font_key
|
10
|
+
return font_key if fonts.key?(font_key)
|
11
|
+
allowed = fonts.keys.map(&:inspect).sort
|
12
|
+
message = "Unrecognized font: '#{font_key.inspect}', expecting one of #{allowed.join(', ')}"
|
13
|
+
raise ArgumentError, message
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s(string, font_key)
|
17
|
+
require 'ruby_figlet'
|
18
|
+
RubyFiglet::Figlet.new(string, fonts[font_key]).to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def fonts
|
22
|
+
return @fonts if @fonts
|
23
|
+
File.exist?(FONT_CACHE) ? load_fonts_from_cache : load_fonts_from_figlet
|
24
|
+
@fonts
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_fonts_from_cache
|
28
|
+
require 'json'
|
29
|
+
@fonts = {}
|
30
|
+
File.open(FONT_CACHE, 'r') do |fp|
|
31
|
+
JSON.parse(fp.read).each do |key, value|
|
32
|
+
@fonts[key.to_sym] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@fonts
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_fonts_from_figlet
|
39
|
+
require 'ruby_figlet'
|
40
|
+
require 'json'
|
41
|
+
@fonts = {}
|
42
|
+
RubyFiglet::Figlet.available.strip.split("\n").each do |font|
|
43
|
+
# Load the font to be sure it exists on the system
|
44
|
+
next unless available?(font)
|
45
|
+
string_key = font.downcase.gsub(/\W/, '')
|
46
|
+
string_key = "_#{string_key}" if string_key[0] !~ /[a-z]/
|
47
|
+
string_key.gsub!(/\d+$/, '')
|
48
|
+
next if @fonts.key?(string_key)
|
49
|
+
@fonts[string_key.to_sym] = font
|
50
|
+
end
|
51
|
+
@fonts[:default] ||= @fonts[PREFERRED_DEFAULT_FONT] || @fonts.values.first
|
52
|
+
File.open(FONT_CACHE, 'w') { |fp| fp.write(JSON.generate(@fonts)) }
|
53
|
+
@fonts
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def available?(font)
|
59
|
+
# Figlet's font loader prints to STDOUT if the font file is not found.
|
60
|
+
# Temporarily redirect STDOUT to /dev/null while the font file is loaded
|
61
|
+
# then remove the redirect in an ensure block. Also, if a font file is
|
62
|
+
# not available, a SystemExit is raise :/
|
63
|
+
original_stdout = $stdout.clone
|
64
|
+
$stdout.reopen(File.new('/dev/null', 'w'))
|
65
|
+
RubyFiglet::Figlet.new('Text', font)
|
66
|
+
true
|
67
|
+
rescue SystemExit, NoMethodError
|
68
|
+
false
|
69
|
+
ensure
|
70
|
+
$stdout.reopen(original_stdout)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/whirled_peas/version.rb
CHANGED
data/whirled_peas.gemspec
CHANGED
@@ -18,11 +18,13 @@ Gem::Specification.new do |spec|
|
|
18
18
|
# Specify which files should be added to the gem when it is released.
|
19
19
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
20
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
21
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|sandbox)/}) }
|
22
22
|
end
|
23
|
+
spec.bindir = 'exe'
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
25
|
spec.require_paths = ['lib']
|
24
26
|
|
25
27
|
spec.add_runtime_dependency 'highline', '~> 2.0'
|
26
28
|
spec.add_runtime_dependency 'json', '~> 2.5'
|
27
|
-
spec.add_runtime_dependency '
|
29
|
+
spec.add_runtime_dependency 'ruby_figlet', '~> 0.6'
|
28
30
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: whirled_peas
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Collier
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-01-
|
11
|
+
date: 2021-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -39,23 +39,24 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '2.5'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: ruby_figlet
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0.
|
47
|
+
version: '0.6'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0.
|
54
|
+
version: '0.6'
|
55
55
|
description:
|
56
56
|
email:
|
57
57
|
- tcollier@gmail.com
|
58
|
-
executables:
|
58
|
+
executables:
|
59
|
+
- whirled_peas
|
59
60
|
extensions: []
|
60
61
|
extra_rdoc_files: []
|
61
62
|
files:
|
@@ -70,26 +71,29 @@ files:
|
|
70
71
|
- Rakefile
|
71
72
|
- bin/console
|
72
73
|
- bin/setup
|
74
|
+
- bin/title_fonts
|
75
|
+
- exe/whirled_peas
|
73
76
|
- lib/whirled_peas.rb
|
77
|
+
- lib/whirled_peas/command_line.rb
|
78
|
+
- lib/whirled_peas/config.rb
|
79
|
+
- lib/whirled_peas/errors.rb
|
74
80
|
- lib/whirled_peas/frame.rb
|
75
|
-
- lib/whirled_peas/frame/
|
76
|
-
- lib/whirled_peas/frame/
|
81
|
+
- lib/whirled_peas/frame/event_loop.rb
|
82
|
+
- lib/whirled_peas/frame/print_consumer.rb
|
77
83
|
- lib/whirled_peas/frame/producer.rb
|
78
84
|
- lib/whirled_peas/null_logger.rb
|
85
|
+
- lib/whirled_peas/template.rb
|
86
|
+
- lib/whirled_peas/template/element.rb
|
87
|
+
- lib/whirled_peas/template/settings.rb
|
79
88
|
- lib/whirled_peas/ui.rb
|
80
|
-
- lib/whirled_peas/ui/ansi.rb
|
81
89
|
- lib/whirled_peas/ui/canvas.rb
|
82
|
-
- lib/whirled_peas/ui/element.rb
|
83
90
|
- lib/whirled_peas/ui/painter.rb
|
84
91
|
- lib/whirled_peas/ui/screen.rb
|
85
|
-
- lib/whirled_peas/
|
86
|
-
- lib/whirled_peas/
|
92
|
+
- lib/whirled_peas/utils.rb
|
93
|
+
- lib/whirled_peas/utils/ansi.rb
|
94
|
+
- lib/whirled_peas/utils/color.rb
|
95
|
+
- lib/whirled_peas/utils/title_font.rb
|
87
96
|
- lib/whirled_peas/version.rb
|
88
|
-
- sandbox/auto.rb
|
89
|
-
- sandbox/box.rb
|
90
|
-
- sandbox/grid.rb
|
91
|
-
- sandbox/sandbox.rb
|
92
|
-
- sandbox/text.rb
|
93
97
|
- whirled_peas.gemspec
|
94
98
|
homepage: https://github.com/tcollier/whirled_peas
|
95
99
|
licenses:
|
@@ -1,61 +0,0 @@
|
|
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
|
@@ -1,56 +0,0 @@
|
|
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
|
@@ -1,199 +0,0 @@
|
|
1
|
-
require_relative 'settings'
|
2
|
-
|
3
|
-
module WhirledPeas
|
4
|
-
module UI
|
5
|
-
class Element
|
6
|
-
attr_accessor :preferred_width, :preferred_height
|
7
|
-
attr_reader :settings
|
8
|
-
|
9
|
-
def initialize(settings)
|
10
|
-
@settings = settings
|
11
|
-
end
|
12
|
-
end
|
13
|
-
private_constant :Element
|
14
|
-
|
15
|
-
class TextElement < Element
|
16
|
-
attr_reader :value
|
17
|
-
|
18
|
-
def initialize(settings)
|
19
|
-
super(TextSettings.merge(settings))
|
20
|
-
end
|
21
|
-
|
22
|
-
def value=(val)
|
23
|
-
@value = val
|
24
|
-
@preferred_width = settings.width || value.length
|
25
|
-
@preferred_height = 1
|
26
|
-
end
|
27
|
-
|
28
|
-
def inspect(indent='')
|
29
|
-
dims = unless preferred_width.nil?
|
30
|
-
"#{indent + ' '}Dimensions: #{preferred_width}x#{preferred_height}"
|
31
|
-
end
|
32
|
-
[
|
33
|
-
"#{indent}#{self.class.name}",
|
34
|
-
dims,
|
35
|
-
"#{indent + ' '}Settings",
|
36
|
-
settings.inspect(indent + ' ')
|
37
|
-
].compact.join("\n")
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
class ComposableElement < Element
|
42
|
-
def initialize(settings)
|
43
|
-
super
|
44
|
-
end
|
45
|
-
|
46
|
-
def children
|
47
|
-
@children ||= []
|
48
|
-
end
|
49
|
-
|
50
|
-
def add_text(&block)
|
51
|
-
element = TextElement.new(settings)
|
52
|
-
element.value = yield nil, element.settings
|
53
|
-
children << element
|
54
|
-
end
|
55
|
-
|
56
|
-
def add_box(&block)
|
57
|
-
element = BoxElement.new(settings)
|
58
|
-
value = yield element, element.settings
|
59
|
-
children << element
|
60
|
-
if element.children.empty? && value.is_a?(String)
|
61
|
-
element.add_text { value }
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def add_grid(&block)
|
66
|
-
element = GridElement.new(settings)
|
67
|
-
values = yield element, element.settings
|
68
|
-
children << element
|
69
|
-
if element.children.empty? && values.is_a?(Array)
|
70
|
-
values.each { |v| element.add_text { v } }
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def inspect(indent='')
|
75
|
-
kids = children.map { |c| c.inspect(indent + ' ') }.join("\n")
|
76
|
-
dims = unless preferred_width.nil?
|
77
|
-
"#{indent + ' '}Dimensions: #{preferred_width}x#{preferred_height}"
|
78
|
-
end
|
79
|
-
[
|
80
|
-
"#{indent}#{self.class.name}",
|
81
|
-
dims,
|
82
|
-
"#{indent + ' '}Settings",
|
83
|
-
settings.inspect(indent + ' '),
|
84
|
-
"#{indent + ' '}Children",
|
85
|
-
kids
|
86
|
-
].compact.join("\n")
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
class Template < ComposableElement
|
91
|
-
def initialize(settings=TemplateSettings.new)
|
92
|
-
super(settings)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
class BoxElement < ComposableElement
|
97
|
-
attr_writer :content_width, :content_height
|
98
|
-
|
99
|
-
def initialize(settings)
|
100
|
-
super(BoxSettings.merge(settings))
|
101
|
-
end
|
102
|
-
|
103
|
-
def self.from_template(template, width, height)
|
104
|
-
box = new(template.settings)
|
105
|
-
template.children.each { |c| box.children << c }
|
106
|
-
box.content_width = box.preferred_width = width
|
107
|
-
box.content_height = box.preferred_height = height
|
108
|
-
box
|
109
|
-
end
|
110
|
-
|
111
|
-
def content_width
|
112
|
-
@content_width ||= begin
|
113
|
-
child_widths = children.map(&:preferred_width)
|
114
|
-
width = settings.horizontal_flow? ? child_widths.sum : (child_widths.max || 0)
|
115
|
-
[width, *settings.width].max
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def preferred_width
|
120
|
-
@preferred_width ||= settings.margin.left +
|
121
|
-
(settings.border.left? ? 1 : 0) +
|
122
|
-
settings.padding.left +
|
123
|
-
content_width +
|
124
|
-
settings.padding.right +
|
125
|
-
(settings.border.right? ? 1 : 0) +
|
126
|
-
settings.margin.right
|
127
|
-
end
|
128
|
-
|
129
|
-
def content_height
|
130
|
-
@content_height ||= begin
|
131
|
-
child_heights = children.map(&:preferred_height)
|
132
|
-
settings.vertical_flow? ? child_heights.sum : (child_heights.max || 0)
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def preferred_height
|
137
|
-
@preferred_height ||= settings.margin.top +
|
138
|
-
(settings.border.top? ? 1 : 0) +
|
139
|
-
settings.padding.top +
|
140
|
-
content_height +
|
141
|
-
settings.padding.bottom +
|
142
|
-
(settings.border.bottom? ? 1 : 0) +
|
143
|
-
settings.margin.bottom
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
class GridElement < ComposableElement
|
148
|
-
def initialize(settings)
|
149
|
-
super(GridSettings.merge(settings))
|
150
|
-
end
|
151
|
-
|
152
|
-
def col_width
|
153
|
-
return @col_width if @col_width
|
154
|
-
@col_width = 0
|
155
|
-
children.each do |child|
|
156
|
-
@col_width = child.preferred_width if child.preferred_width > @col_width
|
157
|
-
end
|
158
|
-
@col_width
|
159
|
-
end
|
160
|
-
|
161
|
-
def row_height
|
162
|
-
return @row_height if @row_height
|
163
|
-
@row_height = 0
|
164
|
-
children.each do |child|
|
165
|
-
@row_height = child.preferred_height if child.preferred_height > @row_height
|
166
|
-
end
|
167
|
-
@row_height
|
168
|
-
end
|
169
|
-
|
170
|
-
|
171
|
-
def preferred_width
|
172
|
-
settings.margin.left +
|
173
|
-
(settings.border.left? ? 1 : 0) +
|
174
|
-
settings.num_cols * (
|
175
|
-
settings.padding.left +
|
176
|
-
col_width +
|
177
|
-
settings.padding.right
|
178
|
-
) +
|
179
|
-
(settings.num_cols - 1) * (settings.border.inner_vert? ? 1 : 0) +
|
180
|
-
(settings.border.right? ? 1 : 0) +
|
181
|
-
settings.margin.right
|
182
|
-
end
|
183
|
-
|
184
|
-
def preferred_height
|
185
|
-
num_rows = (children.length / settings.num_cols).ceil
|
186
|
-
settings.margin.top +
|
187
|
-
(settings.border.top? ? 1 : 0) +
|
188
|
-
num_rows * (
|
189
|
-
settings.padding.top +
|
190
|
-
row_height +
|
191
|
-
settings.padding.bottom
|
192
|
-
) +
|
193
|
-
(num_rows - 1) * (settings.border.inner_horiz? ? 1 : 0) +
|
194
|
-
(settings.border.bottom? ? 1 : 0) +
|
195
|
-
settings.margin.bottom
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|