Class: Cani::Api::Feature::Viewer
- Inherits:
-
Object
- Object
- Cani::Api::Feature::Viewer
- Defined in:
- lib/cani/api/feature/viewer.rb
Constant Summary collapse
- COLOR_PAIRS =
{ # foreground colors # green on default # (legend supported, feature status, percentage counter) 69 => [70, -1], # orange on default # (legend partial, percentage counter) 213 => [208, -1], # red on default # (legend unsupported, percentage counter) 195 => [160, -1], # magenta on default # (legend flag, current feature status) 133 => [134, -1], 12 => [75, -1], # blue on default (legend prefix) 204 => [205, -1], # pink on default (legend polyfill) 99 => [239, -1], # gray on default (legend unknown) # note background + foreground colors 71 => [22, 70], # dark green on green (supported feature) 209 => [130, 208], # dark orange on orange (partial feature) 197 => [88, 160], # dark red on red (unsupported feature) 135 => [91, 134], # dark magenta on magenta (flag features) 13 => [27, 75], # dark blue on blue (prefix feature) 101 => [235, 239], # dark gray on gray (unknown features) 206 => [127, 205], # dark pink on pink (polyfill features) # background colors 70 => [7, 70], # white on green (supported feature) 208 => [7, 208], # white on orange (partial feature) 196 => [7, 160], # white on red (unsupported feature) 134 => [7, 134], # white on magenta (flag features) 11 => [7, 75], # white on blue (prefix feature) 100 => [7, 239], # white on gray (unknown features) 205 => [7, 205], # white on pink (polyfill features) # misc / one-off 254 => [238, 255], # black on light gray (browser names, legend title) 239 => [232, 236] }.freeze
- COLORS =
{ # table headers header: { fg: Curses.color_pair(254), bg: Curses.color_pair(254) }, # current era border era_border: { fg: Curses.color_pair(239), bg: Curses.color_pair(239) }, # support types default: { fg: Curses.color_pair(69), bg: Curses.color_pair(70) }, partial: { fg: Curses.color_pair(213), bg: Curses.color_pair(208) }, prefix: { fg: Curses.color_pair(12), bg: Curses.color_pair(11) }, polyfill: { fg: Curses.color_pair(204), bg: Curses.color_pair(205) }, flag: { fg: Curses.color_pair(133), bg: Curses.color_pair(134) }, unknown: { fg: Curses.color_pair(99), bg: Curses.color_pair(100) }, unsupported: { fg: Curses.color_pair(195), bg: Curses.color_pair(196) }, # statuses un: { fg: Curses.color_pair(213), bg: Curses.color_pair(208) }, ot: { fg: Curses.color_pair(133), bg: Curses.color_pair(134) } }.freeze
- NOTE_COLORS =
{ default: { fg: Curses.color_pair(71), bg: Curses.color_pair(70) }, partial: { fg: Curses.color_pair(209), bg: Curses.color_pair(208) }, prefix: { fg: Curses.color_pair(13), bg: Curses.color_pair(11) }, polyfill: { fg: Curses.color_pair(206), bg: Curses.color_pair(205) }, flag: { fg: Curses.color_pair(135), bg: Curses.color_pair(134) }, unknown: { fg: Curses.color_pair(101), bg: Curses.color_pair(100) }, unsupported: { fg: Curses.color_pair(197), bg: Curses.color_pair(196) } }.freeze
- PERCENT_COLORS =
{ 70..101 => { fg: Curses.color_pair(69), bg: Curses.color_pair(70) }, 40..70 => { fg: Curses.color_pair(213), bg: Curses.color_pair(208) }, 0..40 => { fg: Curses.color_pair(195), bg: Curses.color_pair(196) } }.freeze
- ERAS =
range of eras to show around current era (incl current)
6
- COMPACT =
column width at which to compress the layout
60
- PADDING =
horizontal cell padding
1
- MARKET_SHARE_THRESHHOLD =
predefined market share threshhold
0.5
Instance Attribute Summary collapse
-
#browsers ⇒ Object
readonly
Returns the value of attribute browsers.
-
#col_width ⇒ Object
readonly
Returns the value of attribute col_width.
-
#feature ⇒ Object
readonly
Returns the value of attribute feature.
-
#height ⇒ Object
readonly
Returns the value of attribute height.
-
#table_width ⇒ Object
readonly
Returns the value of attribute table_width.
-
#viewable ⇒ Object
readonly
Returns the value of attribute viewable.
-
#width ⇒ Object
readonly
Returns the value of attribute width.
Instance Method Summary collapse
- #close(*_args) ⇒ Object
- #color(key, type = :bg, source = COLORS) ⇒ Object
- #colw ⇒ Object
- #compact? ⇒ Boolean
- #draw ⇒ Object
-
#initialize(feature, browsers = Cani.api.browsers) ⇒ Viewer
constructor
A new instance of Viewer.
- #note_color(status) ⇒ Object
- #percent_color(percent) ⇒ Object
- #render ⇒ Object
- #resize ⇒ Object
- #status_color(status) ⇒ Object
- #tablew ⇒ Object
Constructor Details
#initialize(feature, browsers = Cani.api.browsers) ⇒ Viewer
Returns a new instance of Viewer.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/cani/api/feature/viewer.rb', line 99 def initialize(feature, browsers = Cani.api.browsers) @feature = feature @browsers = browsers @viewable = browsers.size resize Curses.init_screen Curses.curs_set 0 Curses.noecho if Curses.has_colors? Curses.use_default_colors Curses.start_color end COLOR_PAIRS.each do |(cn, clp)| Curses.init_pair cn, *clp end trap('INT', &method(:close)) at_exit(&method(:close)) end |
Instance Attribute Details
#browsers ⇒ Object (readonly)
Returns the value of attribute browsers.
5 6 7 |
# File 'lib/cani/api/feature/viewer.rb', line 5 def browsers @browsers end |
#col_width ⇒ Object (readonly)
Returns the value of attribute col_width.
5 6 7 |
# File 'lib/cani/api/feature/viewer.rb', line 5 def col_width @col_width end |
#feature ⇒ Object (readonly)
Returns the value of attribute feature.
5 6 7 |
# File 'lib/cani/api/feature/viewer.rb', line 5 def feature @feature end |
#height ⇒ Object (readonly)
Returns the value of attribute height.
5 6 7 |
# File 'lib/cani/api/feature/viewer.rb', line 5 def height @height end |
#table_width ⇒ Object (readonly)
Returns the value of attribute table_width.
5 6 7 |
# File 'lib/cani/api/feature/viewer.rb', line 5 def table_width @table_width end |
#viewable ⇒ Object (readonly)
Returns the value of attribute viewable.
5 6 7 |
# File 'lib/cani/api/feature/viewer.rb', line 5 def viewable @viewable end |
#width ⇒ Object (readonly)
Returns the value of attribute width.
5 6 7 |
# File 'lib/cani/api/feature/viewer.rb', line 5 def width @width end |
Instance Method Details
#close(*_args) ⇒ Object
123 124 125 |
# File 'lib/cani/api/feature/viewer.rb', line 123 def close(*_args) Curses.close_screen end |
#color(key, type = :bg, source = COLORS) ⇒ Object
411 412 413 414 415 416 417 418 |
# File 'lib/cani/api/feature/viewer.rb', line 411 def color(key, type = :bg, source = COLORS) target = key.to_s.downcase.to_sym type = type.to_sym source.find { |(k, _)| k == target }.to_a .fetch(1, {}) .fetch(type, source[:default][type]) end |
#colw ⇒ Object
387 388 389 390 391 |
# File 'lib/cani/api/feature/viewer.rb', line 387 def colw colw = PADDING * 2 + browsers[0..viewable].map(&:max_column_width).max colw.even? ? colw : colw + 1 end |
#compact? ⇒ Boolean
407 408 409 |
# File 'lib/cani/api/feature/viewer.rb', line 407 def compact? width < COMPACT end |
#draw ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/cani/api/feature/viewer.rb', line 127 def draw Curses.clear outer_width = table_width + 2 percent_num = format '%.2f%%', feature.percent status_format = "[#{feature.status}]" percent_label = compact? ? '' : 'support: ' legend_format = 'legend'.center outer_width notes_format = 'notes'.center outer_width offset_x = ((width - outer_width) / 2.0).floor offset_y = 1 cy = 0 # positioning and drawing of percentage perc_num_xs = outer_width - percent_num.size Curses.setpos offset_y + cy, offset_x + perc_num_xs Curses.attron percent_color(feature.percent) do Curses.addstr percent_num end # positioning and drawing of 'support: ' text # ditch this part all together when in compact mode unless compact? perc_lbl_xs = perc_num_xs - percent_label.size Curses.setpos offset_y + cy, offset_x + perc_lbl_xs Curses.addstr percent_label end # draw possibly multi-line feature title title_size = [table_width - percent_num.size - percent_label.size - status_format.size - 3, 1].max title_size += status_format.size if compact? title_chunks = feature.title.chars.each_slice(title_size).map(&:join) title_chunks.each do |part| Curses.setpos offset_y + cy, offset_x Curses.addstr part cy += 1 end # status positioning and drawing # when compact? draw it on the second line instead of the # first line at the end of the title cy += 1 status_yp = offset_y + (compact? ? 1 : 0) status_xp = offset_x + (compact? ? table_width - status_format.size : [title_size, feature.title.size].min + 1) Curses.setpos status_yp, status_xp Curses.attron status_color(feature.status) do Curses.addstr status_format end # 'more or less' predict a height that is too small # since we don't know the entire height but draw # it line-by-line at the moment compact_height = height <= 40 # by default, notes are only shown if visible in the actual table # this means there might be more notes than actually displayed # but this allows us to optimally use available space to display # the most useful information to the user # TODO: provide a config setting to disable this behaviour notes_visible = [] # meaty part, loop through browsers to create # the final feature table relevant_era_count = browsers[0...viewable].map do |browser| era_idx = browser.most_popular_era_idx era_range = (era_idx - (ERAS / 2.0).floor + 1)..(era_idx + (ERAS / 2.0).ceil) era_range.map do |cur_era| era = browser.eras[cur_era].to_s browser.usage[era].to_f >= MARKET_SHARE_THRESHHOLD || (!era.empty? && cur_era >= era_idx - 1) end.select { |x| x }.size end.max browsers[0...viewable].each.with_index do |browser, x| # some set up to find the current era for each browser # and creating a range around that to show past / coming support era_idx = browser.most_popular_era_idx era_range = (era_idx - (relevant_era_count / 2.0).floor + 1)..(era_idx + (relevant_era_count / 2.0).ceil) bx = offset_x + x * col_width + x by = offset_y + cy # draw browser names Curses.setpos by, bx Curses.attron color(:header) do Curses.addstr browser.name.tr('_', '.').center(col_width) end # accordingly increment current browser y for the table header (browser names) # and an additional empty line below the table header by += 3 # draw era's for the current browser era_range.each.with_index do |cur_era, y| era = browser.eras[cur_era].to_s supp_type = feature.support_in(browser.name, era) colr = color supp_type is_current = cur_era == era_idx past_curr = cur_era > era_idx top_pad = 1 bot_pad = compact_height ? 0 : 1 ey = by + (y * (2 + top_pad + bot_pad)) + (bot_pad.zero? && past_curr ? 1 : 0) note_nums = feature.browser_note_nums.fetch(browser.name, {}) .fetch(era, []) # do not draw era's that exceed screen height break if (ey + (is_current ? 1 : bot_pad) + 1) >= height # draw current era outline before drawing all era cells on top if is_current Curses.setpos ey - top_pad - 1, [bx - 1, 0].max Curses.attron(color(:era_border)) { Curses.addstr ' ' * (col_width + 2) } Curses.setpos ey + (is_current ? 1 : bot_pad) + 1, [bx - 1, 0].max Curses.attron(color(:era_border)) { Curses.addstr ' ' * (col_width + 2) } end # only show visible / relevant browsers # era's can either be empty or too new to determine # their usefulness by usage (when newer than current era). if browser.usage[era].to_f >= MARKET_SHARE_THRESHHOLD || (!era.empty? && cur_era >= era_idx - 1) ((ey - top_pad)..(ey + (is_current ? 1 : bot_pad))).each do |ry| txt = (bot_pad.zero? && !is_current) ? (ry >= ey + (is_current ? 1 : bot_pad) ? era.to_s : ' ') : (ry == ey ? era.to_s : ' ') Curses.setpos ry, bx Curses.attron(colr) { Curses.addstr txt.center(col_width) } # draw current ara border inbetween the cells if is_current Curses.setpos ry, bx - 1 Curses.attron(color(:era_border)) { Curses.addstr ' ' } Curses.setpos ry, offset_x + table_width + 2 Curses.attron(color(:era_border)) { Curses.addstr ' ' } end end if note_nums.any? notes_visible.concat(note_nums).uniq! Curses.setpos ey - top_pad, bx Curses.attron(note_color(supp_type)) { Curses.addstr ' ' + note_nums.join(' ') } end end end end # increment current y by amount of eras # plus the 4 lines around the current era # plus the 1 line of browser names # plus the 2 blank lines above and below the eras cy += (relevant_era_count - 1) * (compact_height ? 3 : 4) + relevant_era_count + (relevant_era_count % 2 == 0 ? 0 : 1) if height > cy + 3 # print legend header Curses.setpos offset_y + cy, offset_x Curses.attron color(:header) do Curses.addstr legend_format end end # increment current y by 2 # one for the header line # plus one for a blank line below it cy += 2 # loop through all features to create a legend # showing which label belongs to which color if height > cy + 1 Feature::TYPES.values.each_slice viewable do |group| # draw legend texts at proper position group.compact.each.with_index do |type, lx| Curses.setpos offset_y + cy, offset_x + lx * col_width + lx Curses.attron color(type[:name], :fg) do Curses.addstr "#{type[:short]}(#{type[:symbol]})".center(col_width) end end # if there is more than one group, print the next # group on a new line cy += 1 end end # add extra empty line after legend cy += 1 notes_chunked = feature.notes.map { |nt| nt.chars.each_slice(outer_width).map(&:join).map(&:strip) } filter_vis = Cani.config.notes == 'relevant' ? notes_visible.map(&:to_s) : feature.notes_by_num.keys num_chunked = feature.notes_by_num .select { |(k, _)| filter_vis.include? k } .each_with_object({}) { |(k, nt), h| h[k] = nt.chars.each_slice(outer_width - 5).map(&:join).map(&:strip) } notes_total = (notes_chunked.map(&:size) + num_chunked.map(&:size)).reduce(0) { |total, add| total + add } if height > cy + 2 && (notes_chunked.any? || num_chunked.any?) # print notes header Curses.setpos offset_y + cy, offset_x Curses.attron color(:header) do Curses.addstr notes_format end end # add two new lines, one for the notes header # and one empty line below it cy += 2 # print global notes, wrapped on terminal width notes_chunked.each do |chunks| break if cy + 1 + chunks.size > height chunks.each do |part| Curses.setpos offset_y + cy, offset_x Curses.addstr part cy += 1 end cy += 1 end # print numbered notes, wrapped on terminal width num_chunked.each do |num, chunks| break if cy + 1 + chunks.size > height Curses.setpos offset_y + cy, offset_x Curses.attron color(:header) do Curses.addstr num.center(3) end chunks.each do |part| Curses.setpos offset_y + cy, offset_x + 5 Curses.addstr part cy += 1 end cy += 1 end Curses.refresh end |
#note_color(status) ⇒ Object
420 421 422 |
# File 'lib/cani/api/feature/viewer.rb', line 420 def note_color(status) color status, :fg, NOTE_COLORS end |
#percent_color(percent) ⇒ Object
428 429 430 431 432 |
# File 'lib/cani/api/feature/viewer.rb', line 428 def percent_color(percent) PERCENT_COLORS.find { |(r, _)| r.include? percent }.to_a .fetch(1, {}) .fetch(:fg, COLORS[:unknown][:fg]) end |
#render ⇒ Object
372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/cani/api/feature/viewer.rb', line 372 def render loop do Curses.clear draw key = Curses.getch case key when Curses::KEY_RESIZE then resize else break unless key.nil? end end close end |
#resize ⇒ Object
397 398 399 400 401 402 403 404 405 |
# File 'lib/cani/api/feature/viewer.rb', line 397 def resize @height, @width = IO.console.winsize @viewable = browsers.size @viewable -= 1 while tablew >= @width @col_width = [colw, Feature::TYPES.map { |(_, h)| h[:short].size }.max + 3].max @table_width = tablew - 2 # vertical padding at start and end of current era line end |
#status_color(status) ⇒ Object
424 425 426 |
# File 'lib/cani/api/feature/viewer.rb', line 424 def status_color(status) color status, :fg end |
#tablew ⇒ Object
393 394 395 |
# File 'lib/cani/api/feature/viewer.rb', line 393 def tablew colw * viewable + viewable - 1 end |