brrowser 0.1.0 → 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/README.md +8 -6
- data/bin/brrowser +159 -20
- data/lib/brrowser/fetcher.rb +7 -0
- data/lib/brrowser/renderer.rb +145 -4
- metadata +3 -3
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/README.md
CHANGED
|
@@ -22,7 +22,7 @@ A terminal web browser combining w3m-style rendering with qutebrowser-style keyb
|
|
|
22
22
|
- Ad blocking via StevenBlack hosts list
|
|
23
23
|
|
|
24
24
|
**Navigation:**
|
|
25
|
-
- `j`/`k` or arrows to scroll, `gg`/`G` for top/bottom
|
|
25
|
+
- `j`/`k` or arrows to scroll, `<`/`>` to scroll left/right, `gg`/`G` for top/bottom
|
|
26
26
|
- `TAB`/`S-TAB` to cycle through links and form fields (highlighted with reverse video)
|
|
27
27
|
- `Enter` to follow focused link or edit focused field
|
|
28
28
|
- `o` to open URL, `t` to open in new tab, `O` to edit current URL
|
|
@@ -60,12 +60,13 @@ A terminal web browser combining w3m-style rendering with qutebrowser-style keyb
|
|
|
60
60
|
- `Ctrl-l` to force redraw
|
|
61
61
|
|
|
62
62
|
**Other:**
|
|
63
|
-
- `:download URL` to save files
|
|
63
|
+
- `:download URL` to save files (configurable download folder)
|
|
64
64
|
- Binary files prompt: open with `xdg-open`, download, or cancel
|
|
65
|
-
- `P` for preferences
|
|
65
|
+
- `P` for preferences (image mode, colors, homepage, search engine, download folder)
|
|
66
66
|
- `?` for built-in help page
|
|
67
|
+
- `:about` opens the project GitHub page
|
|
67
68
|
- Cookie persistence across sessions
|
|
68
|
-
- Configurable
|
|
69
|
+
- Configurable homepage (defaults to project page)
|
|
69
70
|
|
|
70
71
|
## Installation
|
|
71
72
|
|
|
@@ -98,6 +99,7 @@ brrowser isene.org # Auto-adds https://
|
|
|
98
99
|
| Key | Action |
|
|
99
100
|
|-----|--------|
|
|
100
101
|
| `j` / `k` / arrows | Scroll up/down |
|
|
102
|
+
| `<` / `>` | Scroll left/right |
|
|
101
103
|
| `Left` / `Right` | Previous/next tab |
|
|
102
104
|
| `gg` / `G` | Top / bottom of page |
|
|
103
105
|
| `Ctrl-d` / `Ctrl-u` | Half page down/up |
|
|
@@ -114,8 +116,8 @@ brrowser isene.org # Auto-adds https://
|
|
|
114
116
|
| `/` | Search page |
|
|
115
117
|
| `n` / `N` | Next/previous search match |
|
|
116
118
|
| `b` / `B` | Bookmark / show bookmarks |
|
|
117
|
-
| `m` +
|
|
118
|
-
| `'` +
|
|
119
|
+
| `m` + key | Set quickmark (0-9, a-z) |
|
|
120
|
+
| `'` + key | Go to quickmark (0-9, a-z) |
|
|
119
121
|
| `f` | Fill and submit form |
|
|
120
122
|
| `y` / `Y` | Copy URL / copy focused element |
|
|
121
123
|
| `e` | Edit page source in $EDITOR |
|
data/bin/brrowser
CHANGED
|
@@ -25,16 +25,17 @@ end
|
|
|
25
25
|
|
|
26
26
|
# brrowser {{{1
|
|
27
27
|
module Brrowser
|
|
28
|
-
VERSION = "0.1.
|
|
29
|
-
HOMEPAGE = "
|
|
28
|
+
VERSION = "0.1.3"
|
|
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")
|
|
32
32
|
|
|
33
33
|
DEFAULTS = {
|
|
34
34
|
"show_images" => true,
|
|
35
35
|
"image_mode" => "auto", # auto, termpix, ascii, off
|
|
36
|
-
"homepage" => "
|
|
36
|
+
"homepage" => "https://github.com/isene/brrowser",
|
|
37
37
|
"search_engine" => "g",
|
|
38
|
+
"download_dir" => "~/Downloads",
|
|
38
39
|
"info_fg" => 252,
|
|
39
40
|
"info_bg" => 236,
|
|
40
41
|
"tab_fg" => 252,
|
|
@@ -49,6 +50,7 @@ module Brrowser
|
|
|
49
50
|
"heading_h1" => 220,
|
|
50
51
|
"heading_h2" => 214,
|
|
51
52
|
"heading_h3" => 208,
|
|
53
|
+
"match_site_colors" => false,
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
HELP_HTML = <<~HTML
|
|
@@ -59,8 +61,8 @@ module Brrowser
|
|
|
59
61
|
<tr><th>Key</th><th>Action</th></tr>
|
|
60
62
|
<tr><td>j / Down</td><td>Scroll down</td></tr>
|
|
61
63
|
<tr><td>k / Up</td><td>Scroll up</td></tr>
|
|
62
|
-
<tr><td><
|
|
63
|
-
<tr><td>>
|
|
64
|
+
<tr><td><</td><td>Scroll left</td></tr>
|
|
65
|
+
<tr><td>></td><td>Scroll right</td></tr>
|
|
64
66
|
<tr><td>Left</td><td>Previous tab</td></tr>
|
|
65
67
|
<tr><td>Right</td><td>Next tab</td></tr>
|
|
66
68
|
<tr><td>gg</td><td>Go to top</td></tr>
|
|
@@ -123,8 +125,8 @@ module Brrowser
|
|
|
123
125
|
<tr><th>Key</th><th>Action</th></tr>
|
|
124
126
|
<tr><td>b</td><td>Bookmark current page</td></tr>
|
|
125
127
|
<tr><td>B</td><td>Show bookmarks</td></tr>
|
|
126
|
-
<tr><td>m +
|
|
127
|
-
<tr><td>' +
|
|
128
|
+
<tr><td>m + key</td><td>Set quickmark (0-9, a-z)</td></tr>
|
|
129
|
+
<tr><td>' + key</td><td>Go to quickmark (0-9, a-z)</td></tr>
|
|
128
130
|
<tr><td>i</td><td>Toggle images</td></tr>
|
|
129
131
|
<tr><td>p</td><td>Show stored password for site</td></tr>
|
|
130
132
|
<tr><td>I</td><td>AI summary of page</td></tr>
|
|
@@ -162,7 +164,7 @@ module Brrowser
|
|
|
162
164
|
@img_dir = File.join(Dir.tmpdir, "brrowser_images_#{Process.pid}")
|
|
163
165
|
Dir.mkdir(@img_dir) unless Dir.exist?(@img_dir)
|
|
164
166
|
|
|
165
|
-
main_loop(url || HOMEPAGE)
|
|
167
|
+
main_loop(url || @conf["homepage"] || HOMEPAGE)
|
|
166
168
|
end
|
|
167
169
|
|
|
168
170
|
# Pane layout {{{2
|
|
@@ -245,6 +247,15 @@ module Brrowser
|
|
|
245
247
|
# Navigation {{{2
|
|
246
248
|
def navigate(url)
|
|
247
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
|
|
248
259
|
@status_msg = "Loading #{url}..."
|
|
249
260
|
refresh_cmd
|
|
250
261
|
|
|
@@ -265,6 +276,7 @@ module Brrowser
|
|
|
265
276
|
@focus_index = -1
|
|
266
277
|
@focused_element = nil
|
|
267
278
|
@highlighted_line = nil
|
|
279
|
+
@h_scroll = 0
|
|
268
280
|
|
|
269
281
|
# Handle image URLs: display the image instead of raw binary
|
|
270
282
|
if ct.match?(/image\//) || result[:url].to_s.match?(/\.(png|jpe?g|gif|bmp|webp|svg|ico)(\?|$)/i)
|
|
@@ -305,6 +317,13 @@ module Brrowser
|
|
|
305
317
|
current_tab.links = page[:links]
|
|
306
318
|
current_tab.images = page[:images]
|
|
307
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)
|
|
308
327
|
check_autofill
|
|
309
328
|
else
|
|
310
329
|
current_tab.title = result[:url]
|
|
@@ -312,6 +331,8 @@ module Brrowser
|
|
|
312
331
|
current_tab.links = []
|
|
313
332
|
current_tab.images = []
|
|
314
333
|
current_tab.forms = []
|
|
334
|
+
@pMain.fg = @conf["content_fg"]
|
|
335
|
+
@pMain.bg = @conf["content_bg"]
|
|
315
336
|
end
|
|
316
337
|
current_tab.ix = 0
|
|
317
338
|
@pMain.ix = 0
|
|
@@ -346,7 +367,7 @@ module Brrowser
|
|
|
346
367
|
end
|
|
347
368
|
|
|
348
369
|
def re_render
|
|
349
|
-
return unless current_tab.url && current_tab.url
|
|
370
|
+
return unless current_tab.url && !current_tab.url.start_with?("about:")
|
|
350
371
|
result = @fetcher.fetch(current_tab.url)
|
|
351
372
|
render_page(result)
|
|
352
373
|
end
|
|
@@ -392,7 +413,7 @@ module Brrowser
|
|
|
392
413
|
end
|
|
393
414
|
|
|
394
415
|
def resolve_search(input)
|
|
395
|
-
return input if input.match?(%r{^https?://}) || input.match?(%r{^file://})
|
|
416
|
+
return input if input.match?(%r{^https?://}) || input.match?(%r{^file://}) || input.match?(%r{^about:})
|
|
396
417
|
return input if input.include?(".") && !input.include?(" ")
|
|
397
418
|
|
|
398
419
|
parts = input.split(" ", 2)
|
|
@@ -441,7 +462,8 @@ module Brrowser
|
|
|
441
462
|
end
|
|
442
463
|
|
|
443
464
|
def refresh_main
|
|
444
|
-
@
|
|
465
|
+
@original_content = current_tab.content.to_s
|
|
466
|
+
@pMain.text = @original_content
|
|
445
467
|
@pMain.refresh
|
|
446
468
|
end
|
|
447
469
|
|
|
@@ -457,6 +479,27 @@ module Brrowser
|
|
|
457
479
|
end
|
|
458
480
|
|
|
459
481
|
# Scrolling {{{2
|
|
482
|
+
def scroll_left
|
|
483
|
+
@h_scroll = [(@h_scroll || 0) - 10, 0].max
|
|
484
|
+
apply_h_scroll
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def scroll_right
|
|
488
|
+
@h_scroll = (@h_scroll || 0) + 10
|
|
489
|
+
apply_h_scroll
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def apply_h_scroll
|
|
493
|
+
return unless @h_scroll && @h_scroll > 0 && @original_content
|
|
494
|
+
lines = @original_content.split("\n")
|
|
495
|
+
shifted = lines.map do |line|
|
|
496
|
+
pure = line.gsub(/\e\[[0-9;]*m/, "")
|
|
497
|
+
pure.length > @h_scroll ? line[@h_scroll..] || "" : ""
|
|
498
|
+
end
|
|
499
|
+
@pMain.text = shifted.join("\n")
|
|
500
|
+
@pMain.full_refresh
|
|
501
|
+
end
|
|
502
|
+
|
|
460
503
|
def scroll_down
|
|
461
504
|
had_images = @showing_image
|
|
462
505
|
clear_images if @showing_image
|
|
@@ -1170,7 +1213,7 @@ module Brrowser
|
|
|
1170
1213
|
if result[:status] == 200
|
|
1171
1214
|
filename = File.basename(URI.parse(result[:url]).path)
|
|
1172
1215
|
filename = "download" if filename.empty?
|
|
1173
|
-
dir = File.expand_path("~/Downloads")
|
|
1216
|
+
dir = File.expand_path(@conf["download_dir"] || "~/Downloads")
|
|
1174
1217
|
Dir.mkdir(dir) unless Dir.exist?(dir)
|
|
1175
1218
|
path = File.join(dir, filename)
|
|
1176
1219
|
# Avoid overwriting
|
|
@@ -1207,14 +1250,14 @@ module Brrowser
|
|
|
1207
1250
|
end
|
|
1208
1251
|
|
|
1209
1252
|
def set_quickmark
|
|
1210
|
-
@status_msg = "Set quickmark (0-9):"
|
|
1253
|
+
@status_msg = "Set quickmark (0-9, a-z):"
|
|
1211
1254
|
refresh_cmd
|
|
1212
1255
|
chr = getchr
|
|
1213
|
-
if chr && chr.match?(/^[0-
|
|
1256
|
+
if chr && chr.length == 1 && chr.match?(/^[0-9a-z]$/)
|
|
1214
1257
|
load_quickmarks
|
|
1215
1258
|
@quickmarks[chr] = { "url" => current_tab.url, "title" => current_tab.title.to_s }
|
|
1216
1259
|
save_quickmarks
|
|
1217
|
-
@status_msg = "Quickmark #{chr} set: #{current_tab.title}"
|
|
1260
|
+
@status_msg = "Quickmark '#{chr}' set: #{current_tab.title}"
|
|
1218
1261
|
else
|
|
1219
1262
|
@status_msg = "Cancelled"
|
|
1220
1263
|
end
|
|
@@ -1222,15 +1265,15 @@ module Brrowser
|
|
|
1222
1265
|
end
|
|
1223
1266
|
|
|
1224
1267
|
def goto_quickmark
|
|
1225
|
-
@status_msg = "Go to quickmark (0-9):"
|
|
1268
|
+
@status_msg = "Go to quickmark (0-9, a-z):"
|
|
1226
1269
|
refresh_cmd
|
|
1227
1270
|
chr = getchr
|
|
1228
|
-
if chr && chr.match?(/^[0-
|
|
1271
|
+
if chr && chr.length == 1 && chr.match?(/^[0-9a-z]$/)
|
|
1229
1272
|
load_quickmarks
|
|
1230
1273
|
if @quickmarks[chr]
|
|
1231
1274
|
navigate(@quickmarks[chr]["url"])
|
|
1232
1275
|
else
|
|
1233
|
-
@status_msg = "No quickmark #{chr}"
|
|
1276
|
+
@status_msg = "No quickmark '#{chr}'"
|
|
1234
1277
|
refresh_cmd
|
|
1235
1278
|
end
|
|
1236
1279
|
else
|
|
@@ -1470,12 +1513,100 @@ module Brrowser
|
|
|
1470
1513
|
refresh_all
|
|
1471
1514
|
end
|
|
1472
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
|
+
|
|
1473
1602
|
# Preferences {{{2
|
|
1474
1603
|
PREF_SETTINGS = [
|
|
1604
|
+
["Match site colors", "match_site_colors", [true, false]],
|
|
1475
1605
|
["Image mode", "image_mode", %w[auto termpix ascii off]],
|
|
1476
1606
|
["Show images", "show_images", [true, false]],
|
|
1477
1607
|
["Homepage", "homepage", :text],
|
|
1478
1608
|
["Search engine", "search_engine", %w[g ddg w]],
|
|
1609
|
+
["Download folder", "download_dir", :text],
|
|
1479
1610
|
["Info bar fg", "info_fg", :color],
|
|
1480
1611
|
["Info bar bg", "info_bg", :color],
|
|
1481
1612
|
["Tab bar fg", "tab_fg", :color],
|
|
@@ -1493,12 +1624,13 @@ module Brrowser
|
|
|
1493
1624
|
]
|
|
1494
1625
|
|
|
1495
1626
|
def show_preferences
|
|
1496
|
-
pw =
|
|
1497
|
-
ph = PREF_SETTINGS.length + 4
|
|
1627
|
+
pw = 56
|
|
1628
|
+
ph = [PREF_SETTINGS.length + 4, @h - 4].min
|
|
1498
1629
|
px = (@w - pw) / 2
|
|
1499
1630
|
py = (@h - ph) / 2
|
|
1500
1631
|
popup = Pane.new(px, py, pw, ph, 252, 234)
|
|
1501
1632
|
popup.border = true
|
|
1633
|
+
popup.scroll = true
|
|
1502
1634
|
sel = 0
|
|
1503
1635
|
|
|
1504
1636
|
build = lambda do
|
|
@@ -1519,6 +1651,11 @@ module Brrowser
|
|
|
1519
1651
|
lines << ""
|
|
1520
1652
|
lines << " h/l:change Enter:edit q:save & close".fg(245)
|
|
1521
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
|
|
1522
1659
|
popup.refresh
|
|
1523
1660
|
end
|
|
1524
1661
|
|
|
@@ -1530,6 +1667,7 @@ module Brrowser
|
|
|
1530
1667
|
when "ESC"
|
|
1531
1668
|
save_config
|
|
1532
1669
|
apply_colors
|
|
1670
|
+
apply_site_colors
|
|
1533
1671
|
break
|
|
1534
1672
|
when "j", "DOWN"
|
|
1535
1673
|
sel = (sel + 1) % PREF_SETTINGS.length
|
|
@@ -1595,6 +1733,7 @@ module Brrowser
|
|
|
1595
1733
|
"j" => :scroll_down, "k" => :scroll_up,
|
|
1596
1734
|
"DOWN" => :scroll_down, "UP" => :scroll_up,
|
|
1597
1735
|
"LEFT" => :prev_tab, "RIGHT" => :next_tab,
|
|
1736
|
+
"<" => :scroll_left, ">" => :scroll_right,
|
|
1598
1737
|
"G" => :go_bottom,
|
|
1599
1738
|
"d" => :close_tab, "u" => :undo_close_tab,
|
|
1600
1739
|
"o" => :open_url,
|
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
|
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.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Geir Isene
|
|
@@ -42,14 +42,14 @@ dependencies:
|
|
|
42
42
|
name: termpix
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - "
|
|
45
|
+
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
47
|
version: '0.3'
|
|
48
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
54
|
version: '0.3'
|
|
55
55
|
description: A terminal web browser combining w3m-style rendering with qutebrowser-style
|