brrowser 0.1.3 → 0.1.4
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.html +95 -0
- data/bin/brrowser +157 -48
- data/lib/brrowser/fetcher.rb +34 -1
- data/lib/brrowser/renderer.rb +6 -5
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3fa096f37a4682bd1edb910013f6a6273acd8f46ee52304eaf140a4963527a5
|
|
4
|
+
data.tar.gz: 0b266827e97d189f85cbdc4bd33318419c03a1fc7e684b5b7eb7d0460f5093ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 78c2c33f3db1bbbc77230e3a4ba98e691b1434c5566a74c7a97a4d8caa5fe7f55fb779082f1fcf09bdb99e00c7310838169a8a06ac1ca2600f57dd6e16f90d53
|
|
7
|
+
data.tar.gz: 4c1d3863f3830fc1e7f43d69810d2ed558922869d5da489322e953b525abec3a608300778530910c473a37991aea8ec7129598ba24e3061fb943721a69e97298
|
data/README.html
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<html><body>
|
|
2
|
+
<h1>brrowser</h1>
|
|
3
|
+
<p><strong>The terminal web browser with vim-style keys.</strong></p>
|
|
4
|
+
<p>A terminal web browser combining w3m-style rendering with qutebrowser-style keyboard navigation. Renders HTML with styled headings, links, tables, forms, and inline images. No modes, no mouse, just keys.</p>
|
|
5
|
+
|
|
6
|
+
<h2>Features</h2>
|
|
7
|
+
<p><strong>Browsing:</strong> Full HTML rendering, inline images (termpix/chafa), YouTube embeds, tabs, back/forward history, search engines, ad blocking, cookie persistence.</p>
|
|
8
|
+
<p><strong>Navigation:</strong> vim-style keys (j/k/gg/G), TAB through links and form fields, search with /, bookmarks and quickmarks.</p>
|
|
9
|
+
<p><strong>Forms:</strong> Fill and submit with auto-fill from stored passwords. Firefox-style save prompt for new logins.</p>
|
|
10
|
+
<p><strong>Images:</strong> Kitty/sixel/w3m via termpix, ASCII art via chafa. Toggle with i.</p>
|
|
11
|
+
<p><strong>AI:</strong> Press I for an AI summary of the current page.</p>
|
|
12
|
+
<p><strong>Extras:</strong> Edit source in $EDITOR, download files, xdg-open for binaries, configurable colors, site color matching.</p>
|
|
13
|
+
|
|
14
|
+
<h2>Installation</h2>
|
|
15
|
+
<pre>gem install brrowser</pre>
|
|
16
|
+
|
|
17
|
+
<h3>Dependencies</h3>
|
|
18
|
+
<ul>
|
|
19
|
+
<li>Ruby 3.0+</li>
|
|
20
|
+
<li><a href="https://github.com/isene/rcurses">rcurses</a> (terminal UI)</li>
|
|
21
|
+
<li><a href="https://nokogiri.org/">nokogiri</a> (HTML parsing)</li>
|
|
22
|
+
<li><a href="https://github.com/isene/termpix">termpix</a> (image display, optional)</li>
|
|
23
|
+
</ul>
|
|
24
|
+
<p>Optional: <a href="https://hpjansson.org/chafa/">chafa</a> for ASCII art, ImageMagick for image scaling, xclip/xsel/wl-copy for clipboard.</p>
|
|
25
|
+
|
|
26
|
+
<h2>Usage</h2>
|
|
27
|
+
<pre>brrowser # Open this page
|
|
28
|
+
brrowser https://example.com # Open URL directly
|
|
29
|
+
brrowser isene.org # Auto-adds https://</pre>
|
|
30
|
+
|
|
31
|
+
<h2>Keybindings</h2>
|
|
32
|
+
<table>
|
|
33
|
+
<tr><th>Key</th><th>Action</th></tr>
|
|
34
|
+
<tr><td>j / k / arrows</td><td>Scroll up/down</td></tr>
|
|
35
|
+
<tr><td>< / ></td><td>Scroll left/right</td></tr>
|
|
36
|
+
<tr><td>Left / Right</td><td>Previous/next tab</td></tr>
|
|
37
|
+
<tr><td>gg / G</td><td>Top / bottom of page</td></tr>
|
|
38
|
+
<tr><td>Ctrl-d / Ctrl-u</td><td>Half page down/up</td></tr>
|
|
39
|
+
<tr><td>Space / PgDn / PgUp</td><td>Page down/up</td></tr>
|
|
40
|
+
<tr><td>TAB / S-TAB</td><td>Next/previous link or field</td></tr>
|
|
41
|
+
<tr><td>Enter</td><td>Follow link or edit field</td></tr>
|
|
42
|
+
<tr><td>o / O</td><td>Open URL / edit current URL</td></tr>
|
|
43
|
+
<tr><td>t</td><td>Open URL in new tab</td></tr>
|
|
44
|
+
<tr><td>H / Backspace</td><td>Go back</td></tr>
|
|
45
|
+
<tr><td>L</td><td>Go forward</td></tr>
|
|
46
|
+
<tr><td>r</td><td>Reload page</td></tr>
|
|
47
|
+
<tr><td>d</td><td>Close tab</td></tr>
|
|
48
|
+
<tr><td>u</td><td>Undo close tab</td></tr>
|
|
49
|
+
<tr><td>/</td><td>Search page</td></tr>
|
|
50
|
+
<tr><td>n / N</td><td>Next/previous search match</td></tr>
|
|
51
|
+
<tr><td>b / B</td><td>Bookmark / show bookmarks</td></tr>
|
|
52
|
+
<tr><td>m + key</td><td>Set quickmark (0-9, a-z)</td></tr>
|
|
53
|
+
<tr><td>' + key</td><td>Go to quickmark (0-9, a-z)</td></tr>
|
|
54
|
+
<tr><td>f</td><td>Fill and submit form</td></tr>
|
|
55
|
+
<tr><td>y / Y</td><td>Copy URL / copy focused element</td></tr>
|
|
56
|
+
<tr><td>e</td><td>Edit page source in $EDITOR</td></tr>
|
|
57
|
+
<tr><td>Ctrl-g</td><td>Edit field in $EDITOR</td></tr>
|
|
58
|
+
<tr><td>i</td><td>Toggle images</td></tr>
|
|
59
|
+
<tr><td>I</td><td>AI page summary</td></tr>
|
|
60
|
+
<tr><td>p</td><td>Show stored password</td></tr>
|
|
61
|
+
<tr><td>P</td><td>Preferences</td></tr>
|
|
62
|
+
<tr><td>Ctrl-l</td><td>Redraw screen</td></tr>
|
|
63
|
+
<tr><td>?</td><td>Help (detailed)</td></tr>
|
|
64
|
+
<tr><td>q</td><td>Quit</td></tr>
|
|
65
|
+
</table>
|
|
66
|
+
|
|
67
|
+
<h2>Commands</h2>
|
|
68
|
+
<p>Type <code>:</code> to enter command mode.</p>
|
|
69
|
+
<table>
|
|
70
|
+
<tr><th>Command</th><th>Action</th></tr>
|
|
71
|
+
<tr><td>:open URL / :o</td><td>Navigate</td></tr>
|
|
72
|
+
<tr><td>:tabopen URL / :to</td><td>Open in new tab</td></tr>
|
|
73
|
+
<tr><td>:close / :q</td><td>Close tab</td></tr>
|
|
74
|
+
<tr><td>:quit / :qa</td><td>Quit</td></tr>
|
|
75
|
+
<tr><td>:back / :forward</td><td>History navigation</td></tr>
|
|
76
|
+
<tr><td>:bookmark / :bm</td><td>Add bookmark or open by name</td></tr>
|
|
77
|
+
<tr><td>:bookmarks / :bms</td><td>List bookmarks</td></tr>
|
|
78
|
+
<tr><td>:download / :dl</td><td>Download file</td></tr>
|
|
79
|
+
<tr><td>:adblock</td><td>Update ad blocklist</td></tr>
|
|
80
|
+
<tr><td>:password / :pw</td><td>Save password for site</td></tr>
|
|
81
|
+
<tr><td>:about</td><td>Open project GitHub page</td></tr>
|
|
82
|
+
</table>
|
|
83
|
+
|
|
84
|
+
<h2>Configuration</h2>
|
|
85
|
+
<p>Press <strong>P</strong> for preferences: image mode, homepage, search engine, download folder, all colors.</p>
|
|
86
|
+
<p>Data stored in <code>~/.brrowser/</code>: config.yml, bookmarks.yml, quickmarks.yml, passwords.yml, cookies.yml, adblock.txt</p>
|
|
87
|
+
|
|
88
|
+
<h2>Links</h2>
|
|
89
|
+
<ul>
|
|
90
|
+
<li><a href="https://github.com/isene/brrowser">GitHub</a></li>
|
|
91
|
+
<li><a href="https://rubygems.org/gems/brrowser">RubyGems</a></li>
|
|
92
|
+
<li><a href="https://isene.org">Author: Geir Isene</a></li>
|
|
93
|
+
</ul>
|
|
94
|
+
<p>Created by Geir Isene with extensive pair-programming with Claude Code.</p>
|
|
95
|
+
</body></html>
|
data/bin/brrowser
CHANGED
|
@@ -25,15 +25,15 @@ end
|
|
|
25
25
|
|
|
26
26
|
# brrowser {{{1
|
|
27
27
|
module Brrowser
|
|
28
|
-
VERSION = "0.1.
|
|
29
|
-
HOMEPAGE = "
|
|
28
|
+
VERSION = "0.1.4"
|
|
29
|
+
HOMEPAGE = "about:home"
|
|
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" => "about:home",
|
|
37
37
|
"search_engine" => "g",
|
|
38
38
|
"download_dir" => "~/Downloads",
|
|
39
39
|
"info_fg" => 252,
|
|
@@ -87,7 +87,7 @@ module Brrowser
|
|
|
87
87
|
<tr><td>f</td><td>Fill and submit form</td></tr>
|
|
88
88
|
<tr><td>Ctrl-g</td><td>Edit focused field in $EDITOR</td></tr>
|
|
89
89
|
<tr><td>H / Backspace</td><td>Go back</td></tr>
|
|
90
|
-
<tr><td>L</td><td>Go forward</td></tr>
|
|
90
|
+
<tr><td>L / Delete</td><td>Go forward</td></tr>
|
|
91
91
|
<tr><td>r</td><td>Reload page</td></tr>
|
|
92
92
|
</table>
|
|
93
93
|
<h2>Tabs</h2>
|
|
@@ -256,6 +256,10 @@ module Brrowser
|
|
|
256
256
|
refresh_all
|
|
257
257
|
return
|
|
258
258
|
end
|
|
259
|
+
if url == "about:home"
|
|
260
|
+
show_homepage
|
|
261
|
+
return
|
|
262
|
+
end
|
|
259
263
|
@status_msg = "Loading #{url}..."
|
|
260
264
|
refresh_cmd
|
|
261
265
|
|
|
@@ -400,6 +404,7 @@ module Brrowser
|
|
|
400
404
|
|
|
401
405
|
def reload
|
|
402
406
|
return unless current_tab.url
|
|
407
|
+
@fetcher.invalidate_cache(current_tab.url)
|
|
403
408
|
navigate_no_history(current_tab.url)
|
|
404
409
|
end
|
|
405
410
|
|
|
@@ -493,7 +498,7 @@ module Brrowser
|
|
|
493
498
|
return unless @h_scroll && @h_scroll > 0 && @original_content
|
|
494
499
|
lines = @original_content.split("\n")
|
|
495
500
|
shifted = lines.map do |line|
|
|
496
|
-
pure = line.gsub(
|
|
501
|
+
pure = line.gsub(Rcurses::ANSI_RE, "")
|
|
497
502
|
pure.length > @h_scroll ? line[@h_scroll..] || "" : ""
|
|
498
503
|
end
|
|
499
504
|
@pMain.text = shifted.join("\n")
|
|
@@ -501,33 +506,41 @@ module Brrowser
|
|
|
501
506
|
end
|
|
502
507
|
|
|
503
508
|
def scroll_down
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
+
if @showing_image
|
|
510
|
+
clear_images
|
|
511
|
+
@pMain.linedown
|
|
512
|
+
current_tab.ix = @pMain.ix
|
|
513
|
+
show_visible_image
|
|
514
|
+
else
|
|
515
|
+
@pMain.linedown
|
|
516
|
+
current_tab.ix = @pMain.ix
|
|
517
|
+
end
|
|
509
518
|
end
|
|
510
519
|
|
|
511
520
|
def scroll_up
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
521
|
+
if @showing_image
|
|
522
|
+
clear_images
|
|
523
|
+
@pMain.lineup
|
|
524
|
+
current_tab.ix = @pMain.ix
|
|
525
|
+
show_visible_image
|
|
526
|
+
else
|
|
527
|
+
@pMain.lineup
|
|
528
|
+
current_tab.ix = @pMain.ix
|
|
529
|
+
end
|
|
517
530
|
end
|
|
518
531
|
|
|
519
532
|
def page_down
|
|
520
533
|
clear_images if @showing_image
|
|
521
534
|
@pMain.pagedown
|
|
522
535
|
current_tab.ix = @pMain.ix
|
|
523
|
-
show_visible_image
|
|
536
|
+
show_visible_image if image_mode != :off
|
|
524
537
|
end
|
|
525
538
|
|
|
526
539
|
def page_up
|
|
527
540
|
clear_images if @showing_image
|
|
528
541
|
@pMain.pageup
|
|
529
542
|
current_tab.ix = @pMain.ix
|
|
530
|
-
show_visible_image
|
|
543
|
+
show_visible_image if image_mode != :off
|
|
531
544
|
end
|
|
532
545
|
|
|
533
546
|
def half_page_down
|
|
@@ -535,7 +548,7 @@ module Brrowser
|
|
|
535
548
|
half = (@h - 3) / 2
|
|
536
549
|
half.times { @pMain.linedown }
|
|
537
550
|
current_tab.ix = @pMain.ix
|
|
538
|
-
show_visible_image
|
|
551
|
+
show_visible_image if image_mode != :off
|
|
539
552
|
end
|
|
540
553
|
|
|
541
554
|
def half_page_up
|
|
@@ -543,7 +556,7 @@ module Brrowser
|
|
|
543
556
|
half = (@h - 3) / 2
|
|
544
557
|
half.times { @pMain.lineup }
|
|
545
558
|
current_tab.ix = @pMain.ix
|
|
546
|
-
show_visible_image
|
|
559
|
+
show_visible_image if image_mode != :off
|
|
547
560
|
end
|
|
548
561
|
|
|
549
562
|
def go_top
|
|
@@ -551,23 +564,23 @@ module Brrowser
|
|
|
551
564
|
@pMain.ix = 0
|
|
552
565
|
current_tab.ix = 0
|
|
553
566
|
refresh_main
|
|
554
|
-
show_visible_image
|
|
567
|
+
show_visible_image if image_mode != :off
|
|
555
568
|
end
|
|
556
569
|
|
|
557
570
|
def go_bottom
|
|
558
571
|
clear_images if @showing_image
|
|
559
572
|
@pMain.bottom
|
|
560
573
|
current_tab.ix = @pMain.ix
|
|
561
|
-
show_visible_image
|
|
574
|
+
show_visible_image if image_mode != :off
|
|
562
575
|
end
|
|
563
576
|
|
|
564
577
|
def redraw_screen
|
|
565
|
-
clear_images
|
|
578
|
+
clear_images if @showing_image
|
|
566
579
|
@pInfo.full_refresh
|
|
567
580
|
@pTab.full_refresh
|
|
568
581
|
@pMain.full_refresh
|
|
569
582
|
@pCmd.full_refresh
|
|
570
|
-
show_visible_image
|
|
583
|
+
show_visible_image if image_mode != :off
|
|
571
584
|
end
|
|
572
585
|
|
|
573
586
|
# Images {{{2
|
|
@@ -613,14 +626,18 @@ module Brrowser
|
|
|
613
626
|
end
|
|
614
627
|
|
|
615
628
|
def image_mode
|
|
629
|
+
return @image_mode_cache if @image_mode_cache_conf == [@conf["image_mode"], @conf["show_images"]]
|
|
630
|
+
@image_mode_cache_conf = [@conf["image_mode"], @conf["show_images"]]
|
|
616
631
|
mode = @conf["image_mode"]
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
632
|
+
@image_mode_cache = if !@conf["show_images"]
|
|
633
|
+
:off
|
|
634
|
+
else
|
|
635
|
+
case mode
|
|
636
|
+
when "termpix" then @termpix.supported? ? :termpix : :off
|
|
637
|
+
when "ascii" then :ascii
|
|
638
|
+
when "off" then :off
|
|
639
|
+
else @termpix.supported? ? :termpix : (chafa_available? ? :ascii : :off)
|
|
640
|
+
end
|
|
624
641
|
end
|
|
625
642
|
end
|
|
626
643
|
|
|
@@ -718,7 +735,7 @@ module Brrowser
|
|
|
718
735
|
return unless @search_term
|
|
719
736
|
lines = current_tab.content.to_s.split("\n")
|
|
720
737
|
lines.each_with_index do |line, i|
|
|
721
|
-
clean = line.gsub(
|
|
738
|
+
clean = line.gsub(Rcurses::ANSI_RE, "")
|
|
722
739
|
if clean.downcase.include?(@search_term.downcase)
|
|
723
740
|
@search_matches << i
|
|
724
741
|
end
|
|
@@ -829,9 +846,14 @@ module Brrowser
|
|
|
829
846
|
navigate(@focused_element[:data][:href])
|
|
830
847
|
return
|
|
831
848
|
end
|
|
832
|
-
# If focused on a form field
|
|
849
|
+
# If focused on a form field: edit if empty, submit if filled
|
|
833
850
|
if @focused_element && @focused_element[:type] == :field
|
|
834
|
-
|
|
851
|
+
field = @focused_element[:data]
|
|
852
|
+
if field[:value].to_s.strip.empty?
|
|
853
|
+
edit_focused_field
|
|
854
|
+
else
|
|
855
|
+
submit_focused_form
|
|
856
|
+
end
|
|
835
857
|
return
|
|
836
858
|
end
|
|
837
859
|
return if current_tab.links.empty?
|
|
@@ -860,11 +882,56 @@ module Brrowser
|
|
|
860
882
|
val = @pInput.ask("#{field[:placeholder] || field[:name]}: ", field[:value].to_s)
|
|
861
883
|
if val && !val.strip.empty?
|
|
862
884
|
field[:value] = val.strip
|
|
863
|
-
|
|
885
|
+
# Update the rendered content to show the value
|
|
886
|
+
if field[:line]
|
|
887
|
+
lines = current_tab.content.split("\n")
|
|
888
|
+
if field[:line] < lines.length
|
|
889
|
+
old_line = lines[field[:line]].gsub("\e[7m", "").gsub("\e[27m", "")
|
|
890
|
+
# Replace the ________ placeholder with the value
|
|
891
|
+
old_line = old_line.gsub(/________\]/, "#{val.strip}]")
|
|
892
|
+
lines[field[:line]] = "\e[7m#{old_line}\e[27m"
|
|
893
|
+
current_tab.content = lines.join("\n")
|
|
894
|
+
end
|
|
895
|
+
end
|
|
896
|
+
@status_msg = "#{field[:name]} = #{val.strip} (Enter to submit)"
|
|
897
|
+
refresh_main
|
|
864
898
|
end
|
|
865
899
|
refresh_cmd
|
|
866
900
|
end
|
|
867
901
|
|
|
902
|
+
def submit_focused_form
|
|
903
|
+
return unless @focused_element && @focused_element[:type] == :field
|
|
904
|
+
form = @focused_element[:form]
|
|
905
|
+
return unless form
|
|
906
|
+
|
|
907
|
+
params = {}
|
|
908
|
+
form[:fields].each do |f|
|
|
909
|
+
next if f[:name].to_s.empty?
|
|
910
|
+
next if f[:type] == "submit"
|
|
911
|
+
params[f[:name]] = f[:value].to_s
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
action = form[:action]
|
|
915
|
+
action = current_tab.url if action.nil? || action.empty?
|
|
916
|
+
method = form[:method] == "post" ? :post : :get
|
|
917
|
+
|
|
918
|
+
@status_msg = "Submitting..."
|
|
919
|
+
refresh_cmd
|
|
920
|
+
|
|
921
|
+
if method == :post
|
|
922
|
+
result = @fetcher.fetch(action, method: :post, params: params)
|
|
923
|
+
else
|
|
924
|
+
query = URI.encode_www_form(params)
|
|
925
|
+
url = action.include?("?") ? "#{action}&#{query}" : "#{action}?#{query}"
|
|
926
|
+
result = @fetcher.fetch(url)
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
current_tab.navigate(action)
|
|
930
|
+
current_tab.url = result[:url]
|
|
931
|
+
render_page(result)
|
|
932
|
+
refresh_all
|
|
933
|
+
end
|
|
934
|
+
|
|
868
935
|
def edit_field_in_editor
|
|
869
936
|
return unless @focused_element && @focused_element[:type] == :field
|
|
870
937
|
field = @focused_element[:data]
|
|
@@ -1016,6 +1083,37 @@ module Brrowser
|
|
|
1016
1083
|
end
|
|
1017
1084
|
|
|
1018
1085
|
# Help {{{2
|
|
1086
|
+
def show_homepage
|
|
1087
|
+
html_file = nil
|
|
1088
|
+
[File.join(__dir__, "..", "README.html"),
|
|
1089
|
+
File.join(__dir__, "..", "..", "README.html")].each do |path|
|
|
1090
|
+
if File.exist?(path)
|
|
1091
|
+
html_file = path
|
|
1092
|
+
break
|
|
1093
|
+
end
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
unless html_file
|
|
1097
|
+
navigate("https://github.com/isene/brrowser")
|
|
1098
|
+
return
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
html = File.read(html_file)
|
|
1102
|
+
current_tab.navigate("about:home") unless current_tab.url == "about:home"
|
|
1103
|
+
current_tab.url = "about:home"
|
|
1104
|
+
renderer = Renderer.new(@w - 2)
|
|
1105
|
+
page = renderer.render(html)
|
|
1106
|
+
current_tab.title = "brrowser"
|
|
1107
|
+
current_tab.content = page[:text]
|
|
1108
|
+
current_tab.links = page[:links]
|
|
1109
|
+
current_tab.images = []
|
|
1110
|
+
current_tab.forms = []
|
|
1111
|
+
current_tab.ix = 0
|
|
1112
|
+
@pMain.ix = 0
|
|
1113
|
+
@status_msg = ""
|
|
1114
|
+
refresh_all
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1019
1117
|
def show_help
|
|
1020
1118
|
tab = Tab.new
|
|
1021
1119
|
@tabs.insert(@current_tab + 1, tab)
|
|
@@ -1043,7 +1141,7 @@ module Brrowser
|
|
|
1043
1141
|
api_key = File.read(key_file).strip
|
|
1044
1142
|
|
|
1045
1143
|
# Get page text (strip ANSI)
|
|
1046
|
-
text = current_tab.content.to_s.gsub(
|
|
1144
|
+
text = current_tab.content.to_s.gsub(Rcurses::ANSI_RE, "")
|
|
1047
1145
|
if text.strip.empty?
|
|
1048
1146
|
@status_msg = "No content to summarize"
|
|
1049
1147
|
refresh_cmd
|
|
@@ -1634,7 +1732,9 @@ module Brrowser
|
|
|
1634
1732
|
sel = 0
|
|
1635
1733
|
|
|
1636
1734
|
build = lambda do
|
|
1637
|
-
lines = [" brrowser Preferences".b
|
|
1735
|
+
lines = [" brrowser Preferences".b]
|
|
1736
|
+
lines << " h/l:change H/L:+/-10 Enter:edit ESC:close".fg(245)
|
|
1737
|
+
lines << ""
|
|
1638
1738
|
PREF_SETTINGS.each_with_index do |(label, key, type), i|
|
|
1639
1739
|
val = @conf[key]
|
|
1640
1740
|
disp = case type
|
|
@@ -1644,18 +1744,19 @@ module Brrowser
|
|
|
1644
1744
|
val.to_s
|
|
1645
1745
|
end
|
|
1646
1746
|
prefix = i == sel ? " \u25b6 ".fg(220) : " "
|
|
1647
|
-
line = "#{prefix}#{label.ljust(
|
|
1747
|
+
line = "#{prefix}#{label.ljust(18)} \u25c4 #{disp.to_s.ljust(14)} \u25b6"
|
|
1648
1748
|
line = line.r if i == sel
|
|
1649
1749
|
lines << line
|
|
1650
1750
|
end
|
|
1651
|
-
lines << ""
|
|
1652
|
-
lines << " h/l:change Enter:edit q:save & close".fg(245)
|
|
1653
1751
|
popup.text = lines.join("\n")
|
|
1654
|
-
# Keep selected item visible
|
|
1655
|
-
visible = ph - 3
|
|
1656
|
-
line_pos = sel +
|
|
1657
|
-
|
|
1658
|
-
|
|
1752
|
+
# Keep selected item visible in scrollable area
|
|
1753
|
+
visible = ph - 3
|
|
1754
|
+
line_pos = sel + 3 # offset for header + legend + blank
|
|
1755
|
+
if line_pos >= popup.ix + visible
|
|
1756
|
+
popup.ix = line_pos - visible + 1
|
|
1757
|
+
elsif line_pos < popup.ix
|
|
1758
|
+
popup.ix = [line_pos, 0].max
|
|
1759
|
+
end
|
|
1659
1760
|
popup.refresh
|
|
1660
1761
|
end
|
|
1661
1762
|
|
|
@@ -1740,6 +1841,7 @@ module Brrowser
|
|
|
1740
1841
|
"t" => :tabopen_url,
|
|
1741
1842
|
"H" => :go_back, "L" => :go_forward,
|
|
1742
1843
|
"BACK" => :go_back,
|
|
1844
|
+
"DEL" => :go_forward,
|
|
1743
1845
|
"r" => :reload,
|
|
1744
1846
|
"J" => :next_tab, "K" => :prev_tab,
|
|
1745
1847
|
"/" => :search_page,
|
|
@@ -1786,7 +1888,7 @@ module Brrowser
|
|
|
1786
1888
|
end
|
|
1787
1889
|
|
|
1788
1890
|
# Auto-show first visible image when downloads complete
|
|
1789
|
-
if
|
|
1891
|
+
if image_mode != :off && !@showing_image
|
|
1790
1892
|
new_count = @img_cache.size
|
|
1791
1893
|
if new_count > @last_img_count
|
|
1792
1894
|
@last_img_count = new_count
|
|
@@ -1833,12 +1935,19 @@ module Brrowser
|
|
|
1833
1935
|
end
|
|
1834
1936
|
|
|
1835
1937
|
def toggle_images
|
|
1836
|
-
|
|
1837
|
-
|
|
1938
|
+
@conf["show_images"] = !@conf["show_images"]
|
|
1939
|
+
@image_mode_cache_conf = nil # Invalidate cache
|
|
1940
|
+
if !@conf["show_images"]
|
|
1941
|
+
# Force clear all images regardless of state
|
|
1942
|
+
@termpix.clear(x: @pMain.x, y: @pMain.y, width: @pMain.w, height: @pMain.h, term_width: @w, term_height: @h) if @termpix.supported?
|
|
1943
|
+
@pMain.full_refresh
|
|
1944
|
+
@showing_image = false
|
|
1838
1945
|
@status_msg = "Images hidden"
|
|
1839
1946
|
else
|
|
1947
|
+
# Re-trigger downloads if needed, then show
|
|
1948
|
+
start_image_downloads
|
|
1840
1949
|
show_visible_image
|
|
1841
|
-
@status_msg = @showing_image ? "Images shown" : "
|
|
1950
|
+
@status_msg = @showing_image ? "Images shown" : "Downloading images..."
|
|
1842
1951
|
end
|
|
1843
1952
|
refresh_cmd
|
|
1844
1953
|
end
|
data/lib/brrowser/fetcher.rb
CHANGED
|
@@ -10,11 +10,19 @@ module Brrowser
|
|
|
10
10
|
USER_AGENT = "brrowser/0.1 (terminal browser)"
|
|
11
11
|
COOKIE_FILE = File.join(Dir.home, ".brrowser", "cookies.yml")
|
|
12
12
|
|
|
13
|
+
MAX_CACHE = 20
|
|
14
|
+
|
|
13
15
|
def initialize
|
|
14
16
|
@cookies = load_cookies
|
|
17
|
+
@page_cache = {}
|
|
18
|
+
@page_cache_order = []
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
def fetch(url, method: :get, params: nil)
|
|
22
|
+
# Return cached response for GET requests
|
|
23
|
+
if method == :get && !params && @page_cache[url]
|
|
24
|
+
return @page_cache[url]
|
|
25
|
+
end
|
|
18
26
|
# Handle local files
|
|
19
27
|
if url.match?(%r{^file://})
|
|
20
28
|
path = url.sub(%r{^file://}, "")
|
|
@@ -63,12 +71,14 @@ module Brrowser
|
|
|
63
71
|
ct = response["content-type"] || ""
|
|
64
72
|
body = response.body
|
|
65
73
|
body = body.force_encoding("UTF-8") if ct.match?(/text|html|json|xml/)
|
|
66
|
-
|
|
74
|
+
result = {
|
|
67
75
|
body: body,
|
|
68
76
|
url: uri.to_s,
|
|
69
77
|
content_type: ct,
|
|
70
78
|
status: response.code.to_i
|
|
71
79
|
}
|
|
80
|
+
cache_response(url, result) if method == :get
|
|
81
|
+
return result
|
|
72
82
|
else
|
|
73
83
|
return {
|
|
74
84
|
body: "Error #{response.code}: #{response.message}",
|
|
@@ -89,6 +99,29 @@ module Brrowser
|
|
|
89
99
|
|
|
90
100
|
private
|
|
91
101
|
|
|
102
|
+
public
|
|
103
|
+
|
|
104
|
+
def invalidate_cache(url = nil)
|
|
105
|
+
if url
|
|
106
|
+
@page_cache.delete(url)
|
|
107
|
+
@page_cache_order.delete(url)
|
|
108
|
+
else
|
|
109
|
+
@page_cache.clear
|
|
110
|
+
@page_cache_order.clear
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def cache_response(url, result)
|
|
117
|
+
@page_cache[url] = result
|
|
118
|
+
@page_cache_order << url unless @page_cache_order.include?(url)
|
|
119
|
+
while @page_cache_order.length > MAX_CACHE
|
|
120
|
+
evict = @page_cache_order.shift
|
|
121
|
+
@page_cache.delete(evict)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
92
125
|
def store_cookies(uri, response)
|
|
93
126
|
Array(response.get_fields("set-cookie")).each do |raw|
|
|
94
127
|
name_val = raw.split(";").first.strip
|
data/lib/brrowser/renderer.rb
CHANGED
|
@@ -2,6 +2,7 @@ require 'nokogiri'
|
|
|
2
2
|
|
|
3
3
|
module Brrowser
|
|
4
4
|
class Renderer
|
|
5
|
+
ANSI_RE = /\e\[[0-9;]*m/.freeze
|
|
5
6
|
SKIP_ELEMENTS = %w[script style noscript svg head].freeze
|
|
6
7
|
BLOCK_ELEMENTS = %w[
|
|
7
8
|
p div section article aside main header footer nav
|
|
@@ -77,7 +78,7 @@ module Brrowser
|
|
|
77
78
|
words = text.split(/( )/)
|
|
78
79
|
words.each do |word|
|
|
79
80
|
next if word.empty?
|
|
80
|
-
visible = word.gsub(
|
|
81
|
+
visible = word.gsub(ANSI_RE, "")
|
|
81
82
|
if @col + visible.length > @width && @col > 0 && word != " "
|
|
82
83
|
flush_line
|
|
83
84
|
end
|
|
@@ -104,7 +105,7 @@ module Brrowser
|
|
|
104
105
|
text = collect_text(node)
|
|
105
106
|
@line << text.b.fg(220)
|
|
106
107
|
flush_line
|
|
107
|
-
@line << ("═" * [text.gsub(
|
|
108
|
+
@line << ("═" * [text.gsub(ANSI_RE, "").length, @width].min).fg(220)
|
|
108
109
|
flush_line
|
|
109
110
|
ensure_blank_line
|
|
110
111
|
when "h2"
|
|
@@ -321,7 +322,7 @@ module Brrowser
|
|
|
321
322
|
field = "[#{label}: ________]".fg(252)
|
|
322
323
|
@line << field
|
|
323
324
|
@col += label.length + 14
|
|
324
|
-
@current_form[:fields] << { type: type, name: name, value: value, placeholder: placeholder } if @current_form
|
|
325
|
+
@current_form[:fields] << { type: type, name: name, value: value, placeholder: placeholder, line: @output.length } if @current_form
|
|
325
326
|
end
|
|
326
327
|
when "select"
|
|
327
328
|
name = node["name"] || "select"
|
|
@@ -330,12 +331,12 @@ module Brrowser
|
|
|
330
331
|
options = node.css("option").map { |o| { value: o["value"] || o.text, text: o.text.strip } }
|
|
331
332
|
selected = node.at_css("option[selected]")
|
|
332
333
|
val = selected ? (selected["value"] || selected.text) : options.first&.dig(:value)
|
|
333
|
-
@current_form[:fields] << { type: "select", name: name, value: val.to_s, options: options } if @current_form
|
|
334
|
+
@current_form[:fields] << { type: "select", name: name, value: val.to_s, options: options, line: @output.length } if @current_form
|
|
334
335
|
when "textarea"
|
|
335
336
|
name = node["name"] || "text"
|
|
336
337
|
@line << "[#{name}: ________]".fg(252)
|
|
337
338
|
@col += name.length + 14
|
|
338
|
-
@current_form[:fields] << { type: "textarea", name: name, value: node.text } if @current_form
|
|
339
|
+
@current_form[:fields] << { type: "textarea", name: name, value: node.text, line: @output.length } if @current_form
|
|
339
340
|
when "label"
|
|
340
341
|
walk(node)
|
|
341
342
|
when "span"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Geir Isene
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rcurses
|
|
@@ -62,6 +62,7 @@ extensions: []
|
|
|
62
62
|
extra_rdoc_files: []
|
|
63
63
|
files:
|
|
64
64
|
- LICENSE
|
|
65
|
+
- README.html
|
|
65
66
|
- README.md
|
|
66
67
|
- bin/brrowser
|
|
67
68
|
- img/brrowser.svg
|