cani 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 251888e57335b837a8f6d6e917375856174c209901e3cad22915fedc73e5ae5d
4
- data.tar.gz: 25c6288f4c8ce97a9a7ffed32ffd1de0c9d5b3a17ca64df7ef4781d362f32253
3
+ metadata.gz: 9de90bea20bfdaed9282223a8f2326c9a57515cba421460382388152613a92d5
4
+ data.tar.gz: a8dfeba0857de5d4e8a35e3971aa05feb3b1d60aca948d3383357074e449b581
5
5
  SHA512:
6
- metadata.gz: 53defce406c9855805a7e7cc0e173ce978d1bfbc9ec7c2d5fa604e2fa6f28c8394114a461373f642290cd5b460988dd43de688a33c804db5e1c7f3f1a9931e22
7
- data.tar.gz: 822dc5a1ec329b0ecb95be0dfdff821a2d2a1c5ff97362d3a66d37fb74627247df49381914db4889df178823d53abc26df6a515c64f4d30250a5f2c89026f14e
6
+ metadata.gz: 63e0d0bf53d8e72915b0c5fe5f7997e7f3dd5341b973dc37c3180fe3b4a91c7f35370d43f57ccc717c24ad3d12a42b6a9bbb413de5afb2901ae0ed66b4c4678d
7
+ data.tar.gz: 514d3cf80000acd736d86f4bc5fdb991ed6d43a986cb6469760be5d92b4e1620ad4331fd34078906e17a3f1d16d17db11c52d2e4d63abba9560cee436b01d19b
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cani (0.1.1)
4
+ cani (0.1.2)
5
5
  colorize
6
+ curses
6
7
  json
7
8
 
8
9
  GEM
@@ -10,6 +11,7 @@ GEM
10
11
  specs:
11
12
  coderay (1.1.2)
12
13
  colorize (0.8.1)
14
+ curses (1.2.4)
13
15
  diff-lcs (1.3)
14
16
  json (2.1.0)
15
17
  method_source (0.9.0)
data/cani.gemspec CHANGED
@@ -23,10 +23,11 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = ['lib']
24
24
 
25
25
  spec.add_runtime_dependency 'colorize'
26
+ spec.add_runtime_dependency 'curses'
26
27
  spec.add_runtime_dependency 'json'
27
28
 
28
29
  spec.add_development_dependency 'bundler', '~> 1.16'
29
- spec.add_development_dependency 'rake', '~> 10.0'
30
30
  spec.add_development_dependency 'pry'
31
+ spec.add_development_dependency 'rake', '~> 10.0'
31
32
  spec.add_development_dependency 'rspec', '~> 3.0'
32
33
  end
data/exe/cani CHANGED
@@ -4,17 +4,4 @@
4
4
  require 'bundler/setup'
5
5
  require 'cani'
6
6
 
7
- # extract first non-option (options starting with '-' or '--')
8
- # argument in ARGV.
9
- cmd ||= Cani.api.config.args.first
10
-
11
- # refresh all completions after new data has been fetched
12
- Cani::Completions.install! if Cani.api.updated?
13
-
14
- if cmd && Cani.respond_to?(cmd)
15
- # if the command is recognized, execute it
16
- Cani.send cmd
17
- else
18
- # otherwise, display help message
19
- Cani.help
20
- end
7
+ Cani.exec! ARGV[0], *ARGV[1..-1]
@@ -1,7 +1,7 @@
1
1
  module Cani
2
2
  class Api
3
3
  class Browser
4
- attr_reader :name, :title, :prefix, :type, :versions, :usage, :abbr, :label
4
+ attr_reader :name, :title, :prefix, :type, :versions, :usage, :abbr, :label, :eras
5
5
 
6
6
  ABBR_MAP = { 'ios' => 'saf.ios' }.freeze
7
7
  LABEL_MAP = {
@@ -34,12 +34,30 @@ module Cani
34
34
  @title = attributes['browser'].downcase
35
35
  @prefix = attributes['prefix'].downcase
36
36
  @type = attributes['type'].downcase
37
- @usage = attributes['usage_global']
37
+ @usage = attributes['usage_global'].each_with_object({}) do |(v, u), h|
38
+ v.split('-').each { |ver| h[ver] = u }
39
+ end
40
+ @eras = attributes['versions'].each_with_object([]) do |v, a|
41
+ if v
42
+ v.split('-').each { |ver| a << ver }
43
+ else
44
+ a << v
45
+ end
46
+ end
38
47
  @versions = @usage.keys
48
+ @features = {}
49
+ end
50
+
51
+ def most_popular_era_idx
52
+ eras.find_index usage.sort_by { |_, v| -v }.first.first
53
+ end
54
+
55
+ def max_column_width
56
+ [name.size, versions.map(&:size).max].max
39
57
  end
40
58
 
41
59
  def features_for(version)
42
- @features ||= Cani.api.features.each_with_object({}) do |ft, h|
60
+ @features[version] ||= Cani.api.features.each_with_object({}) do |ft, h|
43
61
  type = ft.support_in name, version
44
62
  (h[type] ||= []) << { support: type, title: ft.title,
45
63
  status: ft.status, percent: ft.percent }
@@ -0,0 +1,293 @@
1
+ module Cani
2
+ class Api
3
+ class Feature
4
+ class Viewer
5
+ attr_reader :width, :height, :feature, :browsers, :viewable, :col_width, :table_width
6
+
7
+ COLOR_PAIRS = {
8
+ # foreground colors
9
+ 69 => [70, -1], # green on default (legend supported, feature status, percentage counter)
10
+ 213 => [214, -1], # orange on default (legend partial, percentage counter)
11
+ 195 => [9, -1], # red on default (legend unsupported, percentage counter)
12
+ 133 => [128, -1], # magenta on default (legend flag, current feature status)
13
+ 12 => [75, -1], # blue on default (legend prefix)
14
+ 204 => [205, -1], # pink on default (legend polyfill)
15
+ 99 => [8, -1], # gray on default (legend unknown)
16
+
17
+
18
+ # background colors
19
+ 70 => [7, 70], # white on green (supported feature)
20
+ 208 => [7, 214], # white on orange (partial feature)
21
+ 196 => [7, 9], # white on red (unsupported feature)
22
+ 134 => [7, 128], # white on magenta (flag features)
23
+ 11 => [7, 75], # white on blue (prefix feature)
24
+ 100 => [7, 8], # white on gray (unknown features)
25
+ 205 => [7, 205], # white on pink (polyfill features)
26
+
27
+ # misc / one-off
28
+ 254 => [238, 255], # black on light gray (browser names, legend title)
29
+ }.freeze
30
+
31
+ COLORS = {
32
+ # table headers
33
+ header: {fg: Curses.color_pair(254), bg: Curses.color_pair(254)},
34
+
35
+ # support types
36
+ default: {fg: Curses.color_pair(69), bg: Curses.color_pair(70)},
37
+ partial: {fg: Curses.color_pair(213), bg: Curses.color_pair(208)},
38
+ prefix: {fg: Curses.color_pair(12), bg: Curses.color_pair(11)},
39
+ polyfill: {fg: Curses.color_pair(204), bg: Curses.color_pair(205)},
40
+ flag: {fg: Curses.color_pair(133), bg: Curses.color_pair(134)},
41
+ unsupported: {fg: Curses.color_pair(195), bg: Curses.color_pair(196)},
42
+ unknown: {fg: Curses.color_pair(99), bg: Curses.color_pair(100)},
43
+
44
+ # statuses
45
+ un: {fg: Curses.color_pair(213), bg: Curses.color_pair(208)},
46
+ ot: {fg: Curses.color_pair(133), bg: Curses.color_pair(134)}
47
+ }.freeze
48
+
49
+ PERCENT_COLORS = {
50
+ 70..101 => {fg: Curses.color_pair(69), bg: Curses.color_pair(70)},
51
+ 40..70 => {fg: Curses.color_pair(213), bg: Curses.color_pair(208)},
52
+ 0..40 => {fg: Curses.color_pair(195), bg: Curses.color_pair(196)}
53
+ }.freeze
54
+
55
+ ERAS = 6 # range of eras to show around current era (incl current)
56
+ COMPACT = 60 # column width at which to compress the layout
57
+ PADDING = 1 # horizontal cell padding
58
+ MARGIN = 1 # horizontal cell margin
59
+
60
+ def initialize(feature, browsers = Cani.api.browsers)
61
+ @feature = feature
62
+ @browsers = browsers
63
+ @viewable = browsers.size
64
+
65
+ resize
66
+
67
+ Curses.init_screen
68
+ Curses.curs_set 0
69
+ Curses.noecho
70
+ Curses.cbreak
71
+
72
+ if Curses.has_colors?
73
+ Curses.use_default_colors
74
+ Curses.start_color
75
+ end
76
+
77
+ COLOR_PAIRS.each do |(cn, clp)|
78
+ Curses.init_pair cn, *clp
79
+ end
80
+
81
+ trap('INT', &method(:close))
82
+ at_exit(&method(:close))
83
+ end
84
+
85
+ def close(*args)
86
+ Curses.close_screen
87
+ end
88
+
89
+ def draw
90
+ Curses.clear
91
+
92
+ percent_num = format '%.2f%%', feature.percent
93
+ status_format = "[#{feature.status}]"
94
+ percent_label = compact? ? '' : 'support: '
95
+ legend_format = 'legend'.center table_width
96
+
97
+ title_size = [table_width - percent_num.size - percent_label.size - status_format.size - 3, 1].max
98
+ title_size += status_format.size if compact?
99
+ title_chunks = feature.title.chars.each_slice(title_size).map { |chrs| chrs.compact.join }
100
+
101
+ type_count = Feature::TYPES.keys.size
102
+ offset_x = ((width - table_width) / 2.0).floor
103
+ offset_y = ((height - ERAS - title_chunks.size - 10 - (type_count / [type_count, viewable].min.to_f).ceil) / 2.0).floor
104
+ cy = 0
105
+
106
+ # positioning and drawing of percentage
107
+ perc_num_xs = table_width - percent_num.size
108
+ Curses.setpos offset_y + cy, offset_x + perc_num_xs
109
+ Curses.attron percent_color(feature.percent) do
110
+ Curses.addstr percent_num
111
+ end
112
+
113
+ # positioning and drawing of 'support: ' text
114
+ # ditch this part all together when in compact mode
115
+ unless compact?
116
+ perc_lbl_xs = perc_num_xs - percent_label.size
117
+ Curses.setpos offset_y + cy, offset_x + perc_lbl_xs
118
+ Curses.addstr percent_label
119
+ end
120
+
121
+ # draw possibly multi-line feature title
122
+ title_chunks.each do |part|
123
+ Curses.setpos offset_y + cy, offset_x
124
+ Curses.addstr part
125
+
126
+ cy += 1
127
+ end
128
+
129
+ # status positioning and drawing
130
+ # when compact? draw it on the second line instead of the first line at the end of the title
131
+ cy += 1
132
+ status_yp = offset_y + (compact? ? 1 : 0)
133
+ status_xp = offset_x + (compact? ? table_width - status_format.size
134
+ : [title_size, feature.title.size].min + 1)
135
+
136
+ Curses.setpos status_yp, status_xp
137
+ Curses.attron status_color(feature.status) do
138
+ Curses.addstr status_format
139
+ end
140
+
141
+ # meaty part, loop through browsers to create
142
+ # the final feature table
143
+ browsers[0...viewable].each.with_index do |browser, x|
144
+ # some set up to find the current era for each browser
145
+ # and creating a range around that to show past / coming support
146
+ era_idx = browser.most_popular_era_idx
147
+ era_range = (era_idx - (ERAS / 2.0).floor + 1)..(era_idx + (ERAS / 2.0).ceil)
148
+ bx = offset_x + x * col_width + x
149
+ by = offset_y + cy
150
+
151
+ # draw browser names
152
+ Curses.setpos by, bx
153
+ Curses.attron color(:header) do
154
+ Curses.addstr browser.name.tr('_', '.').center(col_width)
155
+ end
156
+
157
+ # accordingly increment current browser y for the table header (browser names)
158
+ # and an additional empty line below the table header
159
+ by += 2
160
+
161
+ # draw era's for the current browser
162
+ era_range.each.with_index do |cur_era, y|
163
+ era = browser.eras[cur_era].to_s
164
+ colr = color(feature.support_in(browser.name, era))
165
+
166
+ # since the current era versions are displayed as 3-line rows
167
+ # with an empty line before and after them, when we are at the current
168
+ # era we increment era y by an additional 2 for the lines above,
169
+ # whenever we are past the current era, increment by 2 for above plus 2
170
+ # extra lines below the current era
171
+ ey = by + y + (cur_era == era_idx ? 2 : (cur_era > era_idx ? 4 : 0))
172
+
173
+ # only show relevant browsers
174
+ if browser.usage[era].to_i >= 0.5 || (!era.empty? && cur_era >= era_idx)
175
+ Curses.setpos ey, bx
176
+ Curses.attron colr do
177
+ Curses.addstr era.center(col_width)
178
+ end
179
+
180
+ # previously, we only skipped some lines in order to create
181
+ # enough space to create the 3-line current era
182
+ # this snippet fills the line before and after the current era
183
+ # with the same color that the era has for that browser / feature
184
+ if cur_era == era_idx
185
+ [-1, 1].each do |relative_y|
186
+ Curses.setpos ey - relative_y, bx
187
+ Curses.attron colr do
188
+ Curses.addstr ' ' * col_width
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ # increment current y by amount of eras
197
+ # plus the 4 lines around the current era
198
+ # plus the 1 line of browser names
199
+ # plus the 2 blank lines above and below the eras
200
+ cy += ERAS + 7
201
+
202
+ # print legend header
203
+ Curses.setpos offset_y + cy, offset_x
204
+ Curses.attron color(:header) do
205
+ Curses.addstr legend_format
206
+ end
207
+
208
+ # increment current y by 2
209
+ # one for the header line
210
+ # plus one for a blank line below it
211
+ cy += 2
212
+
213
+ # loop through all features to create a legend
214
+ # showing which label belongs to which color
215
+ Feature::TYPES.values.each_slice viewable do |group|
216
+ group.compact.each.with_index do |type, lx|
217
+ Curses.setpos offset_y + cy, offset_x + lx * col_width + lx
218
+ Curses.attron color(type[:name], :fg) do
219
+ Curses.addstr "#{type[:short]}(#{type[:symbol]})".center(col_width)
220
+ end
221
+ end
222
+
223
+ # if there is more than one group, print the next
224
+ # group on a new line
225
+ cy += 1
226
+ end
227
+
228
+ Curses.refresh
229
+ end
230
+
231
+ def render
232
+ loop do
233
+ Curses.clear
234
+ draw
235
+
236
+ key = Curses.getch
237
+ case key
238
+ when Curses::KEY_RESIZE then resize
239
+ else break unless key.nil?
240
+ end
241
+ end
242
+
243
+ close
244
+ end
245
+
246
+ def colw
247
+ colw = PADDING * 2 + browsers[0..viewable].map(&:max_column_width).max
248
+
249
+ colw.even? ? colw : colw + 1
250
+ end
251
+
252
+ def tablew
253
+ colw * viewable + viewable - 1
254
+ end
255
+
256
+ def resize
257
+ @height, @width = IO.console.winsize
258
+ @viewable = browsers.size
259
+
260
+ while tablew > @width
261
+ @viewable -= 1
262
+ end
263
+
264
+ @col_width = [colw, Feature::TYPES.map { |(_, h)| h[:short].size }.max + 3].max
265
+ @table_width = tablew
266
+ end
267
+
268
+ def compact?
269
+ width < COMPACT
270
+ end
271
+
272
+ def color(key, type = :bg)
273
+ target = key.to_s.downcase.to_sym
274
+ type = type.to_sym
275
+
276
+ COLORS.find { |(k, _)| k == target }.to_a
277
+ .fetch(1, {})
278
+ .fetch(type, COLORS[:default][type])
279
+ end
280
+
281
+ def status_color(status)
282
+ color status, :fg
283
+ end
284
+
285
+ def percent_color(percent)
286
+ PERCENT_COLORS.find { |(r, _)| r.include? percent }.to_a
287
+ .fetch(1, {})
288
+ .fetch(:fg, COLORS[:unknown][:fg])
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
@@ -1,7 +1,7 @@
1
1
  module Cani
2
2
  class Api
3
3
  class Feature
4
- attr_reader :title, :status, :spec, :stats, :percent
4
+ attr_reader :title, :status, :spec, :stats, :percent, :name
5
5
 
6
6
  STATUSES = {
7
7
  'rec' => 'rc',
@@ -10,38 +10,42 @@ module Cani
10
10
  }.freeze
11
11
 
12
12
  TYPES = {
13
- 'y' => {symbol: '+', name: :default, short: :def},
14
- 'a' => {symbol: '~', name: :partial, short: :part},
15
- 'n' => {symbol: '-', name: :unsupported, short: :unsupp},
16
- 'p' => {symbol: '#', name: :polyfill, short: :poly},
17
- 'x' => {symbol: '@', name: :prefix, short: :prefix},
18
- 'd' => {symbol: '!', name: :flag, short: :flag},
19
- 'u' => {symbol: '?', name: :unknown, short: :unknown}
13
+ 'y' => {symbol: '+', name: :default, short: :sup},
14
+ 'a' => {symbol: '~', name: :partial, short: :prt},
15
+ 'n' => {symbol: '-', name: :unsupported, short: :not},
16
+ 'p' => {symbol: '#', name: :polyfill, short: :ply},
17
+ 'x' => {symbol: '@', name: :prefix, short: :pfx},
18
+ 'd' => {symbol: '!', name: :flag, short: :flg},
19
+ 'u' => {symbol: '?', name: :unknown, short: :unk}
20
20
  }.freeze
21
21
 
22
22
  def initialize(attributes = {})
23
+ @name = attributes[:name].to_s.downcase
23
24
  @title = attributes['title']
24
25
  @status = STATUSES.fetch attributes['status'], attributes['status']
25
26
  @spec = attributes['spec']
26
27
  @percent = attributes['usage_perc_y']
27
28
  @stats = attributes['stats'].each_with_object({}) do |(k, v), h|
28
- h[k] = v.map { |(vv, s)| [vv.downcase, s.to_s[0] || ''] }.to_h
29
+ h[k] = v.each_with_object({}) do |(vv, s), hh|
30
+ vv.split('-').each { |ver| hh[ver] = s[0] }
31
+ end
29
32
  end
30
33
  end
31
34
 
32
35
  def current_support
33
- @current_support ||= Cani.api.config.browsers.map do |browser|
36
+ @current_support ||= Cani.config.browsers.map do |browser|
34
37
  bridx = Cani.api.browsers.find_index { |brs| brs.name == browser }
35
38
  brwsr = Cani.api.browsers[bridx] unless bridx.nil?
36
- syms = stats[browser].values.map { |s| TYPES[s][:symbol] || '' }
37
- .join.rjust Cani.api.config.versions
39
+ syms = stats[browser].values.compact.last(Cani.config.versions)
40
+ .map { |s| TYPES[s][:symbol] || '' }
41
+ .join.rjust Cani.config.versions
38
42
 
39
43
  syms + brwsr.abbr
40
44
  end
41
45
  end
42
46
 
43
47
  def support_in(browser, version)
44
- TYPES.fetch(stats[browser.to_s][version.to_s], {})
48
+ TYPES.fetch(stats[browser.to_s][version.to_s.downcase], {})
45
49
  .fetch :name, :unknown
46
50
  end
47
51
 
data/lib/cani/api.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  require 'net/http'
2
2
 
3
- require_relative 'api/config'
4
3
  require_relative 'api/browser'
5
4
  require_relative 'api/feature'
5
+ require_relative 'api/feature/viewer'
6
6
 
7
7
  module Cani
8
8
  class Api
@@ -12,9 +12,9 @@ module Cani
12
12
 
13
13
  def load_data(fetch: false)
14
14
  @upd = false
15
- data_file = File.join config.directory, 'caniuse.json'
15
+ data_file = File.join Cani.config.directory, 'caniuse.json'
16
16
  data_exists = File.exist? data_file
17
- data_up_to_date = data_exists ? (Time.now.to_i - File.mtime(data_file).to_i < config.expire.to_i)
17
+ data_up_to_date = data_exists ? (Time.now.to_i - File.mtime(data_file).to_i < Cani.config.expire.to_i)
18
18
  : false
19
19
 
20
20
  if !fetch && data_exists && data_up_to_date
@@ -40,7 +40,7 @@ module Cani
40
40
  end
41
41
 
42
42
  def remove!
43
- data_file = File.join config.directory, 'caniuse.json'
43
+ data_file = File.join Cani.config.directory, 'caniuse.json'
44
44
 
45
45
  File.unlink data_file if File.exist? data_file
46
46
  end
@@ -53,14 +53,19 @@ module Cani
53
53
  @upd
54
54
  end
55
55
 
56
- def config(**opts)
57
- @settings ||= Config.new(**opts)
56
+ def find_feature(name)
57
+ name = Regexp.new name.to_s.downcase.gsub(/(\W)/, '.*')
58
+ idx = features.find_index do |ft|
59
+ ft.title.downcase.match?(name) || ft.name.downcase.match?(name)
60
+ end
61
+
62
+ features[idx] if idx
58
63
  end
59
64
 
60
65
  def find_browser(name)
61
66
  name = name.to_s.downcase
62
67
  idx = browsers.find_index do |bwsr|
63
- [bwsr.title, bwsr.name, bwsr.abbr].include? name
68
+ [bwsr.title, bwsr.name, bwsr.abbr].map(&:downcase).include? name
64
69
  end
65
70
 
66
71
  browsers[idx] if idx
@@ -73,12 +78,12 @@ module Cani
73
78
  end
74
79
 
75
80
  def features
76
- @features ||= @data['data'].values.map(&Feature.method(:new))
81
+ @features ||= @data['data'].map { |(name, info)| Feature.new info.merge(name: name) }
77
82
  end
78
83
 
79
84
  def raw
80
85
  begin
81
- Net::HTTP.get URI(config.source)
86
+ Net::HTTP.get URI(Cani.config.source)
82
87
  rescue
83
88
  nil
84
89
  end
@@ -3,12 +3,21 @@ module Cani
3
3
  def self.generate_fish
4
4
  gem_root = File.join File.dirname(__FILE__), '../../'
5
5
  tpl = File.read File.join(gem_root, 'shell/completions/functions.fish')
6
- shw = Cani.api.browsers.reduce String.new do |acc, browser|
7
- [acc, "complete -f -c cani -n '__fish_cani_using_command show' -a '#{browser.abbr}' -d '#{browser.label}'",
8
- "complete -f -c cani -n '__fish_cani_showing_browser #{browser.abbr}' -a '#{browser.versions.reverse.join(' ')}'"].join("\n")
6
+
7
+ shw = Cani.api.browsers.reduce String.new do |acc, browser|
8
+ versions = browser.versions.reverse.join(' ')
9
+ acc +
10
+ "\ncomplete -f -c cani -n '__fish_cani_using_command show' -a '#{browser.abbr}' -d '#{browser.label}'" +
11
+ "\ncomplete -f -c cani -n '__fish_cani_showing_browser #{browser.abbr}' -a '#{versions}'"
12
+ end
13
+
14
+ use = Cani.api.features.reduce String.new do |acc, feature|
15
+ description = feature.title.size > 40 ? feature.title[0..28] + '..' : feature.title
16
+ acc +
17
+ "\ncomplete -f -c cani -n '__fish_cani_using_command use' -a '#{feature.name}' -d '#{description}'"
9
18
  end
10
19
 
11
- tpl + shw
20
+ tpl + shw + "\n" + use
12
21
  end
13
22
 
14
23
  def self.generate_zsh
@@ -22,6 +31,7 @@ module Cani
22
31
  end.strip
23
32
 
24
33
  tpl.gsub('{{names}}', Cani.api.browsers.map(&:abbr).join(' '))
34
+ .gsub('{{features}}', Cani.api.features.map(&:name).join(' '))
25
35
  .gsub '{{versions}}', versions
26
36
  end
27
37
 
@@ -36,21 +46,22 @@ module Cani
36
46
  end.strip
37
47
 
38
48
  tpl.gsub('{{names}}', Cani.api.browsers.map(&:abbr).join(' '))
49
+ .gsub('{{features}}', Cani.api.features.map(&:name).join(' '))
39
50
  .gsub '{{versions}}', versions
40
51
  end
41
52
 
42
53
  def self.install!
43
54
  # create all parent folders
44
- FileUtils.mkdir_p Cani.api.config.fish_comp_dir
45
- FileUtils.mkdir_p Cani.api.config.comp_dir
55
+ FileUtils.mkdir_p Cani.config.fish_comp_dir
56
+ FileUtils.mkdir_p Cani.config.comp_dir
46
57
 
47
58
  # write each completion file
48
- File.open File.join(Cani.api.config.fish_comp_dir, 'cani.fish'), 'w' do |file|
59
+ File.open File.join(Cani.config.fish_comp_dir, 'cani.fish'), 'w' do |file|
49
60
  file << generate_fish
50
61
  end
51
62
 
52
63
  %w[bash zsh].each do |shell|
53
- File.open File.join(Cani.api.config.comp_dir, "_cani.#{shell}"), 'w' do |file|
64
+ File.open File.join(Cani.config.comp_dir, "_cani.#{shell}"), 'w' do |file|
54
65
  file << send("generate_#{shell}")
55
66
  end
56
67
  end
@@ -60,12 +71,12 @@ module Cani
60
71
  end
61
72
 
62
73
  def self.remove!
63
- fish_comp = File.join Cani.api.config.fish_comp_dir, 'cani.fish'
74
+ fish_comp = File.join Cani.config.fish_comp_dir, 'cani.fish'
64
75
 
65
76
  File.unlink fish_comp if File.exist? fish_comp
66
77
 
67
78
  %w[bash zsh].each do |shell|
68
- shell_comp = File.join Cani.api.config.comp_dir, "_cani.#{shell}"
79
+ shell_comp = File.join Cani.config.comp_dir, "_cani.#{shell}"
69
80
 
70
81
  File.unlink shell_comp if File.exist? shell_comp
71
82
  end
@@ -78,7 +89,7 @@ module Cani
78
89
  %w[bash zsh].each do |shell|
79
90
  shellrc = File.join Dir.home, ".#{shell}rc"
80
91
  lines = File.read(shellrc).split "\n"
81
- comp_path = File.join Cani.api.config.comp_dir, "_cani.#{shell}"
92
+ comp_path = File.join Cani.config.comp_dir, "_cani.#{shell}"
82
93
  rm_idx = lines.find_index { |l| l.match? comp_path }
83
94
 
84
95
  lines.delete_at rm_idx unless rm_idx.nil?
@@ -90,7 +101,7 @@ module Cani
90
101
  %w[bash zsh].each do |shell|
91
102
  shellrc = File.join Dir.home, ".#{shell}rc"
92
103
  lines = File.read(shellrc).split "\n"
93
- comp_path = File.join Cani.api.config.comp_dir, "_cani.#{shell}"
104
+ comp_path = File.join Cani.config.comp_dir, "_cani.#{shell}"
94
105
  slidx = lines.find_index { |l| l.match? comp_path }
95
106
 
96
107
  if slidx
@@ -0,0 +1,111 @@
1
+ module Cani
2
+ class Config
3
+ attr_reader :settings
4
+
5
+ FILE = File.join(Dir.home, '.config', 'cani', 'config.yml').freeze
6
+ DIRECTORY = File.dirname(FILE).freeze
7
+ COMP_DIR = File.join(DIRECTORY, 'completions').freeze
8
+ FISH_DIR = File.join(Dir.home, '.config', 'fish').freeze
9
+ FISH_COMP_DIR = File.join(FISH_DIR, 'completions').freeze
10
+ DEFAULTS = {
11
+ # data settings
12
+ 'expire' => 86_400,
13
+ 'source' => 'https://raw.githubusercontent.com/Fyrd/caniuse/master/data.json',
14
+
15
+ # usage settings
16
+ 'versions' => 1,
17
+ 'browsers' => %w[ie edge chrome firefox safari ios_saf opera android bb]
18
+ }.freeze
19
+
20
+ def initialize(**opts)
21
+ @settings = DEFAULTS.merge opts
22
+
23
+ if File.exist? file
24
+ if (yml = YAML.load_file(file))
25
+ @settings.merge! yml
26
+ end
27
+ else
28
+ install!
29
+ end
30
+ end
31
+
32
+ def file
33
+ FILE
34
+ end
35
+
36
+ def directory
37
+ DIRECTORY
38
+ end
39
+
40
+ def comp_dir
41
+ COMP_DIR
42
+ end
43
+
44
+ def fish_dir
45
+ FISH_DIR
46
+ end
47
+
48
+ def fish_comp_dir
49
+ FISH_COMP_DIR
50
+ end
51
+
52
+ def remove!
53
+ File.unlink file if File.exist? file
54
+ FileUtils.rm_rf directory if Dir.exist? directory
55
+ end
56
+
57
+ def install!
58
+ hrs = (DEFAULTS['expire'] / 3600.to_f).round 2
59
+ days = (hrs / 24.to_f).round 2
60
+ wk = (days / 7.to_f).round 2
61
+ mo = (days / 30.to_f).round 2
62
+ tstr = if mo >= 1
63
+ "#{mo == mo.to_i ? mo.to_i : mo} month#{mo != 1 ? 's' : ''}"
64
+ elsif wk >= 1
65
+ "#{wk == wk.to_i ? wk.to_i : wk} week#{wk != 1 ? 's' : ''}"
66
+ elsif days >= 1
67
+ "#{days == days.to_i ? days.to_i : days} day#{days != 1 ? 's' : ''}"
68
+ else
69
+ "#{hrs == hrs.to_i ? hrs.to_i : hrs} hour#{hrs != 1 ? 's' : ''}"
70
+ end
71
+
72
+ FileUtils.mkdir_p directory
73
+ File.open file, 'w' do |f|
74
+ f << "---\n"
75
+ f << "# this is the default configuration file for the \"Cani\" RubyGem.\n"
76
+ f << "# it contains some options to control what is shown, when new data\n"
77
+ f << "# is fetched, where it should be fetched from.\n"
78
+ f << "# documentation: https://github.com/sidofc/cani\n"
79
+ f << "# rubygems: https://rubygems.org/gems/cani\n\n"
80
+ f << "# the \"expire\" key defines the interval at which new data is\n"
81
+ f << "# fetched from \"source\". It's value is passed in as seconds\n"
82
+ f << "# default value: #{DEFAULTS['expire']} # => #{tstr}\n"
83
+ f << "expire: #{expire}\n\n"
84
+ f << "# the \"source\" key is used to fetch the data required for\n"
85
+ f << "# this command to work.\n"
86
+ f << "source: #{source}\n\n"
87
+ f << "# the \"versions\" key defines how many versions of support\n"
88
+ f << "# will be shown in the \"use\" command\n"
89
+ f << "# e.g. `-ie +edge` becomes `--ie ++edge` when this is set to 2, etc..."
90
+ f << "versions: #{versions}\n\n"
91
+ f << "# the \"browsers\" key defines which browsers are shown\n"
92
+ f << "# in the \"use\" command\n"
93
+ f << "browsers:\n"
94
+ f << " # enabled:\n"
95
+ f << browsers.map { |bn| " - #{bn}" }.join("\n") + "\n"
96
+ f << " # others:\n"
97
+ f << (Cani.api.browsers.map(&:name) - browsers).map { |bn| " # - #{bn}" }.join("\n")
98
+ end
99
+
100
+ Completions.install!
101
+ end
102
+
103
+ def method_missing(mtd, *args, &block)
104
+ settings.key?(mtd.to_s) ? settings[mtd.to_s] : super
105
+ end
106
+
107
+ def respond_to_missing?(mtd, include_private = false)
108
+ settings.key? mtd.to_s
109
+ end
110
+ end
111
+ end
data/lib/cani/fzf.rb CHANGED
@@ -29,7 +29,7 @@ module Cani
29
29
  end
30
30
 
31
31
  def self.feature_rows
32
- Cani.api.features.map do |ft|
32
+ @feature_rows ||= Cani.api.features.map do |ft|
33
33
  pc = format('%.2f%%', ft.percent).rjust 6
34
34
  tt = format('%-24s', ft.title.size > 24 ? ft.title[0..23].strip + '..'
35
35
  : ft.title)
data/lib/cani/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Cani
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/cani.rb CHANGED
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'io/console'
4
+ require 'curses'
3
5
  require 'colorize'
4
6
  require 'json'
5
7
  require 'yaml'
6
8
 
7
- require 'cani/version'
8
9
  require 'cani/api'
9
10
  require 'cani/fzf'
11
+ require 'cani/config'
12
+ require 'cani/version'
10
13
  require 'cani/completions'
11
14
 
12
15
  # Cani
@@ -15,6 +18,27 @@ module Cani
15
18
  @api ||= Api.new
16
19
  end
17
20
 
21
+ def self.config
22
+ @settings ||= Config.new
23
+ end
24
+
25
+
26
+ def self.exec!(command, *args)
27
+ command = :help unless command && respond_to?(command)
28
+ command = command.to_s.downcase.to_sym
29
+
30
+ case command
31
+ when :use
32
+ use args[0]
33
+ when :show
34
+ show args[0], args[1]
35
+ when :update, :purge, :help, :version, :install_completions
36
+ send command
37
+ else
38
+ help
39
+ end
40
+ end
41
+
18
42
  def self.help
19
43
  puts "Cani #{VERSION} <https://github.com/SidOfc/cani>"
20
44
  puts ''
@@ -57,7 +81,7 @@ module Cani
57
81
  def self.purge
58
82
  Completions.remove!
59
83
  api.remove!
60
- api.config.remove!
84
+ config.remove!
61
85
  end
62
86
 
63
87
  def self.update
@@ -65,16 +89,28 @@ module Cani
65
89
  end
66
90
 
67
91
  def self.edit
68
- system ENV.fetch('EDITOR', 'vim'), api.config.file
92
+ system ENV.fetch('EDITOR', 'vim'), config.file
69
93
  end
70
94
 
71
- def self.use
72
- Fzf.pick Fzf.feature_rows,
73
- header: 'use] [' + Api::Feature.support_legend,
74
- colors: %i[green light_black light_white light_black]
95
+ def self.use(feature = nil)
96
+ if feature && (feature = api.find_feature(feature))
97
+ Api::Feature::Viewer.new(feature).render
98
+ use
99
+ elsif (chosen = Fzf.pick(Fzf.feature_rows,
100
+ header: 'use] [' + Api::Feature.support_legend,
101
+ colors: %i[green light_black light_white light_black]))
102
+
103
+ # chosen[2] is the index of the title column from Fzf.feature_rows
104
+ if chosen.any? && (feature = api.find_feature(chosen[2]))
105
+ Api::Feature::Viewer.new(feature).render
106
+ use
107
+ else
108
+ exit
109
+ end
110
+ end
75
111
  end
76
112
 
77
- def self.show(brws = api.config.args[1], version = api.config.args[2])
113
+ def self.show(brws = nil, version = nil)
78
114
  browser = api.find_browser brws
79
115
 
80
116
  if browser
@@ -83,14 +119,14 @@ module Cani
83
119
  header: "show:#{browser.title.downcase}:#{version}] [#{Api::Feature.support_legend}",
84
120
  colors: [:green, :light_black, :light_white]
85
121
 
86
- show browser.title, nil
122
+ show browser.title
87
123
  else
88
124
  if (version = Fzf.pick(Fzf.browser_usage_rows(browser),
89
125
  header: [:show, browser.title],
90
126
  colors: %i[white light_black]).first)
91
127
  show browser.title, version
92
128
  else
93
- show nil, nil
129
+ show
94
130
  end
95
131
  end
96
132
  else
@@ -98,7 +134,7 @@ module Cani
98
134
  header: [:show],
99
135
  colors: %i[white light_black]).first
100
136
 
101
- show browser.title, nil unless browser.nil?
137
+ show browser.title unless browser.nil?
102
138
  end
103
139
  end
104
140
  end
@@ -15,6 +15,9 @@ _cani_completions() {
15
15
  ;;
16
16
  esac
17
17
  ;;
18
+ "use")
19
+ COMPREPLY=($(compgen -W "{{features}}" "${COMP_WORDS[COMP_CWORD]}"))
20
+ ;;
18
21
  *)
19
22
  COMPREPLY=($(compgen -W "use show help version update purge install_completions" "${COMP_WORDS[COMP_CWORD]}"))
20
23
  ;;
@@ -20,6 +20,9 @@ function _cani {
20
20
  {{versions}}
21
21
  esac
22
22
  ;;
23
+ use)
24
+ _arguments -C "1: :({{features}})"
25
+ ;;
23
26
  esac
24
27
  }
25
28
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cani
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sidney Liebrand
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-07-17 00:00:00.000000000 Z
11
+ date: 2018-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: json
28
+ name: curses
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,33 +39,33 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: bundler
42
+ name: json
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '1.16'
48
- type: :development
47
+ version: '0'
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: '1.16'
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rake
56
+ name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '10.0'
61
+ version: '1.16'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '10.0'
68
+ version: '1.16'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: pry
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rspec
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -118,9 +132,10 @@ files:
118
132
  - lib/cani.rb
119
133
  - lib/cani/api.rb
120
134
  - lib/cani/api/browser.rb
121
- - lib/cani/api/config.rb
122
135
  - lib/cani/api/feature.rb
136
+ - lib/cani/api/feature/viewer.rb
123
137
  - lib/cani/completions.rb
138
+ - lib/cani/config.rb
124
139
  - lib/cani/fzf.rb
125
140
  - lib/cani/version.rb
126
141
  - shell/completions/functions.bash
@@ -1,121 +0,0 @@
1
- module Cani
2
- class Api
3
- class Config
4
- attr_reader :settings
5
-
6
- FILE = File.join(Dir.home, '.config', 'cani', 'config.yml').freeze
7
- DIRECTORY = File.dirname(FILE).freeze
8
- COMP_DIR = File.join(DIRECTORY, 'completions').freeze
9
- FISH_DIR = File.join(Dir.home, '.config', 'fish').freeze
10
- FISH_COMP_DIR = File.join(FISH_DIR, 'completions').freeze
11
- DEFAULTS = {
12
- # data settings
13
- 'expire' => 86_400,
14
- 'source' => 'https://raw.githubusercontent.com/Fyrd/caniuse/master/data.json',
15
-
16
- # usage settings
17
- 'versions' => 1,
18
- 'browsers' => %w[chrome firefox edge ie safari ios_saf opera android bb]
19
- }.freeze
20
-
21
- def initialize(**opts)
22
- @settings = DEFAULTS.merge opts
23
-
24
- if File.exist? file
25
- if (yml = YAML.load_file(file))
26
- @settings.merge! yml
27
- end
28
- else
29
- install!
30
- end
31
- end
32
-
33
- def file
34
- FILE
35
- end
36
-
37
- def directory
38
- DIRECTORY
39
- end
40
-
41
- def comp_dir
42
- COMP_DIR
43
- end
44
-
45
- def fish_dir
46
- FISH_DIR
47
- end
48
-
49
- def fish_comp_dir
50
- FISH_COMP_DIR
51
- end
52
-
53
- def flags
54
- @flags ||= ARGV.select { |arg| arg.start_with? '-' }
55
- end
56
-
57
- def args
58
- @args ||= ARGV.reject { |arg| arg.start_with? '-' }
59
- end
60
-
61
- def remove!
62
- File.unlink file if File.exist? file
63
- FileUtils.rm_rf directory if Dir.exist? directory
64
- end
65
-
66
- def install!
67
- hrs = (DEFAULTS['expire'] / 3600.to_f).round 2
68
- days = (hrs / 24.to_f).round 2
69
- wk = (days / 7.to_f).round 2
70
- mo = (days / 30.to_f).round 2
71
- tstr = if mo >= 1
72
- "#{mo == mo.to_i ? mo.to_i : mo} month#{mo != 1 ? 's' : ''}"
73
- elsif wk >= 1
74
- "#{wk == wk.to_i ? wk.to_i : wk} week#{wk != 1 ? 's' : ''}"
75
- elsif days >= 1
76
- "#{days == days.to_i ? days.to_i : days} day#{days != 1 ? 's' : ''}"
77
- else
78
- "#{hrs == hrs.to_i ? hrs.to_i : hrs} hour#{hrs != 1 ? 's' : ''}"
79
- end
80
-
81
- FileUtils.mkdir_p directory
82
- File.open file, 'w' do |f|
83
- f << "---\n"
84
- f << "# this is the default configuration file for the \"Cani\" RubyGem.\n"
85
- f << "# it contains some options to control what is shown, when new data\n"
86
- f << "# is fetched, where it should be fetched from.\n"
87
- f << "# documentation: https://github.com/sidofc/cani\n"
88
- f << "# rubygems: https://rubygems.org/gems/cani\n\n"
89
- f << "# the \"expire\" key defines the interval at which new data is\n"
90
- f << "# fetched from \"source\". It's value is passed in as seconds\n"
91
- f << "# default value: #{DEFAULTS['expire']} # => #{tstr}\n"
92
- f << "expire: #{expire}\n\n"
93
- f << "# the \"source\" key is used to fetch the data required for\n"
94
- f << "# this command to work.\n"
95
- f << "source: #{source}\n\n"
96
- f << "# the \"versions\" key defines how many versions of support\n"
97
- f << "# will be shown in the \"use\" command\n"
98
- f << "# e.g. `-ie +edge` becomes `--ie ++edge` when this is set to 2, etc..."
99
- f << "versions: #{versions}\n\n"
100
- f << "# the \"browsers\" key defines which browsers are shown\n"
101
- f << "# in the \"use\" command\n"
102
- f << "browsers:\n"
103
- f << " # enabled:\n"
104
- f << browsers.map { |bn| " - #{bn}" }.join("\n") + "\n"
105
- f << " # others:\n"
106
- f << (Cani.api.browsers.map(&:name) - browsers).map { |bn| " # - #{bn}" }.join("\n")
107
- end
108
-
109
- Completions.install!
110
- end
111
-
112
- def method_missing(mtd, *args, &block)
113
- settings.key?(mtd.to_s) ? settings[mtd.to_s] : super
114
- end
115
-
116
- def respond_to_missing?(mtd, include_private = false)
117
- settings.key? mtd.to_s
118
- end
119
- end
120
- end
121
- end