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.
@@ -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