whirled_peas 0.1.0 → 0.4.1

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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module WhirledPeas
2
- VERSION = '0.1.0'
2
+ VERSION = '0.4.1'
3
3
  end
@@ -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{^(test|spec|features)/}) }
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 'tty-cursor', '~> 0.7'
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.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Collier
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-20 00:00:00.000000000 Z
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: tty-cursor
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.7'
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.7'
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/consumer.rb
76
- - lib/whirled_peas/frame/loop.rb
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/ui/settings.rb
86
- - lib/whirled_peas/ui/stroke.rb
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