brrowser 0.1.1 → 0.1.3

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: 8db3fcfbe72fe0a1f0ef54a5b99b9e7c1b3bd4030f4cc37639aa623ca492f17d
4
- data.tar.gz: 19079bf7bf6ab1b632b193bf6847a81530524aac6d526864b9467eb2e1020e37
3
+ metadata.gz: 031bfa105348853d72db747c32797041f5661fe4ca66547f25a3dacb9a4b211a
4
+ data.tar.gz: f89cca0ebc7e8c7e48537ea62865a869df55d0d6213d6b6d574f2e60c5c8dd7c
5
5
  SHA512:
6
- metadata.gz: 2151be2c0a51f2b207594c4b86b9f79b4c9c8507dee66ddc9b52e8b7da149a56ad44db2f33fad762342687fae74d5699beee0f83cca4b3b354342caefbc17823
7
- data.tar.gz: 21df370567d8d3e33cf0826ad240f6ddff96aab5252c671172596fadf9d6b5645673f631e74883e9af74ff6dcaef51a1e9cb6dfc515bbe382fc8a7578d51a134
6
+ metadata.gz: 45309f7af0f927a1fcfd20614826875d3fa7cbb78f8103f43f0ca2c5092a8df5bea05e654d2645fc541e40f5047a259ec7d2416e623727b42eaebabee56e08bd
7
+ data.tar.gz: ea797828fd0810a4086f884e8c3f6cc1c734ec63030e0fcc7fc7b329c71e899c9c7c469f5c0baa6307f3ddabf2e994c44f2da94a09dfc06905c6807a9058e8ff
data/bin/brrowser CHANGED
@@ -25,7 +25,7 @@ end
25
25
 
26
26
  # brrowser {{{1
27
27
  module Brrowser
28
- VERSION = "0.1.1"
28
+ VERSION = "0.1.3"
29
29
  HOMEPAGE = "https://github.com/isene/brrowser"
30
30
  CONFIG_DIR = File.join(Dir.home, ".brrowser")
31
31
  CONFIG_FILE = File.join(CONFIG_DIR, "config.yml")
@@ -50,6 +50,7 @@ module Brrowser
50
50
  "heading_h1" => 220,
51
51
  "heading_h2" => 214,
52
52
  "heading_h3" => 208,
53
+ "match_site_colors" => false,
53
54
  }
54
55
 
55
56
  HELP_HTML = <<~HTML
@@ -246,6 +247,15 @@ module Brrowser
246
247
  # Navigation {{{2
247
248
  def navigate(url)
248
249
  url = resolve_search(url)
250
+ # Handle about: URLs internally
251
+ if url == "about:blank"
252
+ current_tab.navigate(url)
253
+ current_tab.url = url
254
+ current_tab.title = ""; current_tab.content = ""
255
+ current_tab.links = []; current_tab.images = []; current_tab.forms = []
256
+ refresh_all
257
+ return
258
+ end
249
259
  @status_msg = "Loading #{url}..."
250
260
  refresh_cmd
251
261
 
@@ -307,6 +317,13 @@ module Brrowser
307
317
  current_tab.links = page[:links]
308
318
  current_tab.images = page[:images]
309
319
  current_tab.forms = page[:forms]
320
+ colors = page[:colors]
321
+ # If no colors found in HTML, try first external stylesheet
322
+ if @conf["match_site_colors"] && !colors[:bg] && !colors[:fg]
323
+ colors = fetch_external_css_colors(result[:body], result[:url])
324
+ end
325
+ current_tab.instance_variable_set(:@site_colors, colors)
326
+ apply_site_colors(colors)
310
327
  check_autofill
311
328
  else
312
329
  current_tab.title = result[:url]
@@ -314,6 +331,8 @@ module Brrowser
314
331
  current_tab.links = []
315
332
  current_tab.images = []
316
333
  current_tab.forms = []
334
+ @pMain.fg = @conf["content_fg"]
335
+ @pMain.bg = @conf["content_bg"]
317
336
  end
318
337
  current_tab.ix = 0
319
338
  @pMain.ix = 0
@@ -1494,8 +1513,95 @@ module Brrowser
1494
1513
  refresh_all
1495
1514
  end
1496
1515
 
1516
+ def apply_site_colors(colors = nil)
1517
+ colors ||= current_tab.instance_variable_get(:@site_colors) rescue nil
1518
+ if colors && @conf["match_site_colors"]
1519
+ bg = colors[:bg] || @conf["content_bg"]
1520
+ fg = colors[:fg] || @conf["content_fg"]
1521
+ # Ensure high contrast: if bg and fg are too close, pick white or black
1522
+ fg = high_contrast_fg(bg, fg)
1523
+ @pMain.bg = bg
1524
+ @pMain.fg = fg
1525
+ else
1526
+ @pMain.bg = @conf["content_bg"]
1527
+ @pMain.fg = @conf["content_fg"]
1528
+ end
1529
+ end
1530
+
1531
+ # Map xterm-256 color to approximate luminance (0.0 dark, 1.0 bright)
1532
+ def color_luminance(c)
1533
+ r, g, b = xterm256_to_rgb(c)
1534
+ (0.299 * r + 0.587 * g + 0.114 * b) / 255.0
1535
+ end
1536
+
1537
+ def xterm256_to_rgb(c)
1538
+ if c < 16
1539
+ # Standard colors: approximate
1540
+ basic = [
1541
+ [0,0,0],[128,0,0],[0,128,0],[128,128,0],[0,0,128],[128,0,128],[0,128,128],[192,192,192],
1542
+ [128,128,128],[255,0,0],[0,255,0],[255,255,0],[0,0,255],[255,0,255],[0,255,255],[255,255,255]
1543
+ ]
1544
+ basic[c]
1545
+ elsif c < 232
1546
+ # 6x6x6 color cube
1547
+ c -= 16
1548
+ b = (c % 6) * 51
1549
+ g = ((c / 6) % 6) * 51
1550
+ r = (c / 36) * 51
1551
+ [r, g, b]
1552
+ else
1553
+ # Grayscale ramp
1554
+ v = (c - 232) * 10 + 8
1555
+ [v, v, v]
1556
+ end
1557
+ end
1558
+
1559
+ def high_contrast_fg(bg, fg)
1560
+ bg_lum = color_luminance(bg)
1561
+ fg_lum = color_luminance(fg)
1562
+ contrast = (bg_lum - fg_lum).abs
1563
+ if contrast < 0.3
1564
+ # Not enough contrast: use white for dark bg, black for light bg
1565
+ bg_lum < 0.5 ? 255 : 0
1566
+ else
1567
+ fg
1568
+ end
1569
+ end
1570
+
1571
+ def fetch_external_css_colors(html, base_url)
1572
+ doc = Nokogiri::HTML(html)
1573
+ # Find first local stylesheet
1574
+ doc.css("link[rel=stylesheet]").each do |link|
1575
+ href = link["href"].to_s
1576
+ next if href.empty?
1577
+ next if href.match?(/fonts\.googleapis|font-awesome|calendly|bootstrap/i)
1578
+ href = "https:#{href}" if href.start_with?("//")
1579
+ href = URI.join(base_url, href).to_s unless href.match?(%r{^https?://})
1580
+ begin
1581
+ css_result = @fetcher.fetch(href)
1582
+ next unless css_result[:status] == 200
1583
+ css = css_result[:body].to_s.force_encoding("UTF-8").scrub("")
1584
+ bg = nil; fg = nil
1585
+ # Check body { } rule
1586
+ if css.match(/body\s*\{([^}]+)\}/m)
1587
+ block = $1
1588
+ bg_match = block[/background(?:-color)?\s*:\s*([^;]+)/, 1]
1589
+ fg_match = block[/(?<!background-)color\s*:\s*([^;]+)/, 1]
1590
+ r = Renderer.new(80)
1591
+ bg = r.send(:parse_css_color, bg_match.strip) if bg_match
1592
+ fg = r.send(:parse_css_color, fg_match.strip) if fg_match
1593
+ end
1594
+ return { bg: bg, fg: fg } if bg || fg
1595
+ rescue
1596
+ next
1597
+ end
1598
+ end
1599
+ { bg: nil, fg: nil }
1600
+ end
1601
+
1497
1602
  # Preferences {{{2
1498
1603
  PREF_SETTINGS = [
1604
+ ["Match site colors", "match_site_colors", [true, false]],
1499
1605
  ["Image mode", "image_mode", %w[auto termpix ascii off]],
1500
1606
  ["Show images", "show_images", [true, false]],
1501
1607
  ["Homepage", "homepage", :text],
@@ -1518,12 +1624,13 @@ module Brrowser
1518
1624
  ]
1519
1625
 
1520
1626
  def show_preferences
1521
- pw = 44
1522
- ph = PREF_SETTINGS.length + 4
1627
+ pw = 56
1628
+ ph = [PREF_SETTINGS.length + 4, @h - 4].min
1523
1629
  px = (@w - pw) / 2
1524
1630
  py = (@h - ph) / 2
1525
1631
  popup = Pane.new(px, py, pw, ph, 252, 234)
1526
1632
  popup.border = true
1633
+ popup.scroll = true
1527
1634
  sel = 0
1528
1635
 
1529
1636
  build = lambda do
@@ -1544,6 +1651,11 @@ module Brrowser
1544
1651
  lines << ""
1545
1652
  lines << " h/l:change Enter:edit q:save & close".fg(245)
1546
1653
  popup.text = lines.join("\n")
1654
+ # Keep selected item visible
1655
+ visible = ph - 3 # account for border
1656
+ line_pos = sel + 2 # offset for header lines
1657
+ popup.ix = [line_pos - visible + 1, 0].max if line_pos >= popup.ix + visible
1658
+ popup.ix = line_pos if line_pos < popup.ix
1547
1659
  popup.refresh
1548
1660
  end
1549
1661
 
@@ -1555,6 +1667,7 @@ module Brrowser
1555
1667
  when "ESC"
1556
1668
  save_config
1557
1669
  apply_colors
1670
+ apply_site_colors
1558
1671
  break
1559
1672
  when "j", "DOWN"
1560
1673
  sel = (sel + 1) % PREF_SETTINGS.length
@@ -15,6 +15,13 @@ module Brrowser
15
15
  end
16
16
 
17
17
  def fetch(url, method: :get, params: nil)
18
+ # Handle local files
19
+ if url.match?(%r{^file://})
20
+ path = url.sub(%r{^file://}, "")
21
+ body = File.exist?(path) ? File.read(path) : "File not found: #{path}"
22
+ ct = path.match?(/\.html?$/i) ? "text/html" : "text/plain"
23
+ return { body: body, url: url, content_type: ct, status: 200 }
24
+ end
18
25
  url = "https://#{url}" unless url.match?(%r{^https?://})
19
26
  uri = URI.parse(url)
20
27
  redirects = 0
@@ -45,7 +45,8 @@ module Brrowser
45
45
  walk(body)
46
46
  flush_line
47
47
 
48
- { text: @output.join("\n"), links: @links, images: @images, forms: @forms, title: title }
48
+ site_colors = extract_site_colors(doc)
49
+ { text: @output.join("\n"), links: @links, images: @images, forms: @forms, title: title, colors: site_colors }
49
50
  end
50
51
 
51
52
  private
@@ -281,7 +282,11 @@ module Brrowser
281
282
  ensure_blank_line
282
283
  end
283
284
  when "table"
284
- render_table(node)
285
+ if layout_table?(node)
286
+ walk(node) # Treat layout tables as normal content
287
+ else
288
+ render_table(node)
289
+ end
285
290
  when "form"
286
291
  ensure_blank_line
287
292
  action = node["action"] || ""
@@ -335,6 +340,16 @@ module Brrowser
335
340
  walk(node)
336
341
  when "span"
337
342
  walk(node)
343
+ when "td", "th"
344
+ # Reached here from layout table walk
345
+ flush_line if @col > 0
346
+ walk(node)
347
+ flush_line if @col > 0
348
+ when "tr"
349
+ walk(node)
350
+ flush_line if @col > 0
351
+ when "tbody", "thead", "tfoot", "caption"
352
+ walk(node)
338
353
  else
339
354
  walk(node)
340
355
  end
@@ -384,6 +399,22 @@ module Brrowser
384
399
  end
385
400
  end
386
401
 
402
+ def layout_table?(node)
403
+ # Nested tables = layout
404
+ return true if node.css("table").any?
405
+ # Single-column tables = layout
406
+ first_row = node.at_css("tr")
407
+ return true if first_row && first_row.css("td, th").length <= 1
408
+ # Tables containing block elements in cells = layout
409
+ node.css("td, th").first(4).each do |cell|
410
+ return true if cell.at_css("p, div, h1, h2, h3, h4, ul, ol, table, blockquote")
411
+ end
412
+ # Width 100% = layout
413
+ style = node["style"].to_s + " " + (node["width"].to_s)
414
+ return true if style.match?(/100%/)
415
+ false
416
+ end
417
+
387
418
  def render_table(table_node)
388
419
  ensure_blank_line
389
420
  rows = []
@@ -437,9 +468,7 @@ module Brrowser
437
468
  end
438
469
 
439
470
  def render_table_vertical(rows, table_node)
440
- # Render each row as a block with label: value pairs
441
471
  headers = table_node.css("th").any? ? rows.shift : nil
442
-
443
472
  rows.each do |row|
444
473
  row.each_with_index do |cell, ci|
445
474
  next if cell.strip.empty?
@@ -449,5 +478,117 @@ module Brrowser
449
478
  @output << (" " * @indent) + ("\u2500" * 20).fg(240)
450
479
  end
451
480
  end
481
+
482
+ # Site color extraction {{{
483
+ def extract_site_colors(doc)
484
+ bg = nil; fg = nil
485
+
486
+ # 1. HTML attributes (old-school)
487
+ body = doc.at_css("body")
488
+ if body
489
+ bg = parse_css_color(body["bgcolor"]) if body["bgcolor"]
490
+ fg = parse_css_color(body["text"]) if body["text"]
491
+ end
492
+
493
+ # 2. Inline styles on body/html
494
+ [body, doc.at_css("html")].compact.each do |node|
495
+ style = node["style"].to_s
496
+ next if style.empty?
497
+ bg ||= extract_css_color(style, /background(?:-color)?\s*:\s*([^;]+)/)
498
+ fg ||= extract_css_color(style, /(?<!background-)color\s*:\s*([^;]+)/)
499
+ end
500
+
501
+ # 3. Embedded <style> blocks - check body, html, :root rules
502
+ unless bg && fg
503
+ doc.css("style").each do |style_node|
504
+ css = style_node.text
505
+ %w[body html :root .page .site .wrapper #page #wrapper #content].each do |sel|
506
+ pattern = /#{Regexp.escape(sel)}\s*\{([^}]+)\}/m
507
+ if css.match(pattern)
508
+ block = $1
509
+ bg ||= extract_css_color(block, /background(?:-color)?\s*:\s*([^;]+)/)
510
+ fg ||= extract_css_color(block, /(?<!background-)color\s*:\s*([^;]+)/)
511
+ end
512
+ end
513
+ # CSS variables: --bg-color, --background, --text-color, etc.
514
+ css.scan(/--(bg|background|main-bg|site-bg|page-bg)[^:]*:\s*([^;}\n]+)/) do |_, val|
515
+ bg ||= parse_css_color(val.strip)
516
+ end
517
+ css.scan(/--(fg|text|color|main-color|text-color|font-color)[^:]*:\s*([^;}\n]+)/) do |_, val|
518
+ fg ||= parse_css_color(val.strip)
519
+ end
520
+ end
521
+ end
522
+
523
+ # 4. Meta theme-color
524
+ unless bg
525
+ meta = doc.at_css('meta[name="theme-color"]')
526
+ bg = parse_css_color(meta["content"]) if meta && meta["content"]
527
+ end
528
+
529
+ { bg: bg, fg: fg }
530
+ end
531
+
532
+ def extract_css_color(css_text, regex)
533
+ return nil unless css_text.match?(regex)
534
+ val = css_text[regex, 1]&.strip
535
+ parse_css_color(val)
536
+ end
537
+
538
+ def parse_css_color(val)
539
+ return nil unless val
540
+ val = val.strip.downcase
541
+
542
+ # Hex colors
543
+ if val.match?(/^#[0-9a-f]{6}$/)
544
+ return hex_to_256(val)
545
+ elsif val.match?(/^#[0-9a-f]{3}$/)
546
+ expanded = "##{val[1]*2}#{val[2]*2}#{val[3]*2}"
547
+ return hex_to_256(expanded)
548
+ end
549
+
550
+ # rgb(r, g, b)
551
+ if val.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/)
552
+ r, g, b = $1.to_i, $2.to_i, $3.to_i
553
+ return rgb_to_256(r, g, b)
554
+ end
555
+
556
+ # Named colors (common ones)
557
+ named = {
558
+ "white" => 15, "black" => 0, "red" => 1, "green" => 2,
559
+ "blue" => 4, "yellow" => 3, "cyan" => 6, "magenta" => 5,
560
+ "gray" => 245, "grey" => 245, "silver" => 250,
561
+ "darkgray" => 238, "darkgrey" => 238,
562
+ "lightgray" => 252, "lightgrey" => 252,
563
+ "navy" => 17, "teal" => 30, "maroon" => 1,
564
+ "olive" => 3, "purple" => 5, "aqua" => 14,
565
+ "orange" => 208, "pink" => 218,
566
+ "whitesmoke" => 255, "ghostwhite" => 255,
567
+ "aliceblue" => 153, "ivory" => 255,
568
+ }
569
+ named[val.gsub(/\s/, "")]
570
+ end
571
+
572
+ def hex_to_256(hex)
573
+ r = hex[1..2].to_i(16)
574
+ g = hex[3..4].to_i(16)
575
+ b = hex[5..6].to_i(16)
576
+ rgb_to_256(r, g, b)
577
+ end
578
+
579
+ def rgb_to_256(r, g, b)
580
+ # Check grayscale ramp (232-255) first
581
+ if r == g && g == b
582
+ return 16 if r < 8
583
+ return 231 if r > 248
584
+ return (((r - 8).to_f / 247 * 24).round + 232)
585
+ end
586
+ # Map to 6x6x6 color cube (16-231)
587
+ ri = ((r.to_f / 255) * 5).round
588
+ gi = ((g.to_f / 255) * 5).round
589
+ bi = ((b.to_f / 255) * 5).round
590
+ 16 + (36 * ri) + (6 * gi) + bi
591
+ end
592
+ # }}}
452
593
  end
453
594
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brrowser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene