cani 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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