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 +4 -4
- data/bin/brrowser +116 -3
- data/lib/brrowser/fetcher.rb +7 -0
- data/lib/brrowser/renderer.rb +145 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 031bfa105348853d72db747c32797041f5661fe4ca66547f25a3dacb9a4b211a
|
|
4
|
+
data.tar.gz: f89cca0ebc7e8c7e48537ea62865a869df55d0d6213d6b6d574f2e60c5c8dd7c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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 =
|
|
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
|
data/lib/brrowser/fetcher.rb
CHANGED
|
@@ -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
|
data/lib/brrowser/renderer.rb
CHANGED
|
@@ -45,7 +45,8 @@ module Brrowser
|
|
|
45
45
|
walk(body)
|
|
46
46
|
flush_line
|
|
47
47
|
|
|
48
|
-
|
|
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
|
-
|
|
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
|