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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 031bfa105348853d72db747c32797041f5661fe4ca66547f25a3dacb9a4b211a
4
- data.tar.gz: f89cca0ebc7e8c7e48537ea62865a869df55d0d6213d6b6d574f2e60c5c8dd7c
3
+ metadata.gz: b3fa096f37a4682bd1edb910013f6a6273acd8f46ee52304eaf140a4963527a5
4
+ data.tar.gz: 0b266827e97d189f85cbdc4bd33318419c03a1fc7e684b5b7eb7d0460f5093ce
5
5
  SHA512:
6
- metadata.gz: 45309f7af0f927a1fcfd20614826875d3fa7cbb78f8103f43f0ca2c5092a8df5bea05e654d2645fc541e40f5047a259ec7d2416e623727b42eaebabee56e08bd
7
- data.tar.gz: ea797828fd0810a4086f884e8c3f6cc1c734ec63030e0fcc7fc7b329c71e899c9c7c469f5c0baa6307f3ddabf2e994c44f2da94a09dfc06905c6807a9058e8ff
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>&lt; / &gt;</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.3"
29
- HOMEPAGE = "https://github.com/isene/brrowser"
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" => "https://github.com/isene/brrowser",
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(/\e\[[0-9;]*m/, "")
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
- had_images = @showing_image
505
- clear_images if @showing_image
506
- @pMain.linedown
507
- current_tab.ix = @pMain.ix
508
- show_visible_image if had_images
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
- had_images = @showing_image
513
- clear_images if @showing_image
514
- @pMain.lineup
515
- current_tab.ix = @pMain.ix
516
- show_visible_image if had_images
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
- return :off unless @conf["show_images"]
618
- case mode
619
- when "termpix" then @termpix.supported? ? :termpix : :off
620
- when "ascii" then :ascii
621
- when "off" then :off
622
- else # auto
623
- @termpix.supported? ? :termpix : (chafa_available? ? :ascii : :off)
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(/\e\[[0-9;]*m/, "")
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, edit it
849
+ # If focused on a form field: edit if empty, submit if filled
833
850
  if @focused_element && @focused_element[:type] == :field
834
- edit_focused_field
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
- @status_msg = "Set #{field[:name]} = #{val.strip}"
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(/\e\[[0-9;]*m/, "")
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(16)} \u25c4 #{disp.to_s.ljust(12)} \u25b6"
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 # 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
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 @termpix.supported? && !@showing_image
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
- if @showing_image
1837
- clear_images
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" : "No images in view"
1950
+ @status_msg = @showing_image ? "Images shown" : "Downloading images..."
1842
1951
  end
1843
1952
  refresh_cmd
1844
1953
  end
@@ -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
- return {
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
@@ -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(/\e\[[0-9;]*m/, "")
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(/\e\[[0-9;]*m/, "").length, @width].min).fg(220)
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.3
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-27 00:00:00.000000000 Z
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