sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit a3d6ea4087e8493d71b2e5154a04b1fc5a85827d
parent 55c9afceac1eda9f2a66023983ec05a55cd073a4
Author: Dan Callaghan <djc@djc.id.au>
Date:   Sun,  5 Apr 2026 12:42:41 +1000

refactor LineCursorMode to handle initial load

Rather than having every calling site explicitly load the initial set of
threads whenever one of the subclasses of LineCursorMode is spawned,
make LineCursorMode itself handle the initial load. This way, the
logic for deciding how many threads to load at what time is consolidated
into LineCursorMode.

Diffstat:
M bin/sup | 8 +++++---
M lib/sup/buffer.rb | 1 +
M lib/sup/mode.rb | 1 +
M lib/sup/modes/contact_list_mode.rb | 1 -
M lib/sup/modes/label_search_results_mode.rb | 3 +--
M lib/sup/modes/line_cursor_mode.rb | 2 ++
M lib/sup/modes/search_results_mode.rb | 1 -
M lib/sup/modes/thread_view_mode.rb | 3 +--
M test/unit/test_line_cursor_mode.rb | 13 ++++++++++---
9 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/bin/sup b/bin/sup
@@ -206,7 +206,10 @@ begin
     end
   end unless $opts[:no_initial_poll]
 
-  imode.load_threads :num => ibuf.content_height, :when_done => lambda { |num| reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }
+  reporting_thread("poll after loading inbox") do
+    sleep 1
+    PollManager.poll
+  end unless $opts[:no_threads] || $opts[:no_initial_poll]
 
   if $opts[:compose]
     to = Person.from_address_list $opts[:compose]
@@ -328,8 +331,7 @@ begin
         BufferManager.spawn "Edit message", r
         r.default_edit_message
       else
-        b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
-        b.mode.load_threads :num => b.content_height if new
+        BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
       end
     when :show_inbox
       BufferManager.raise_to_front ibuf
diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb
@@ -330,6 +330,7 @@ EOS
     w = Ncurses.stdscr
     b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => opts[:force_to_top], :system => opts[:system]
     mode.buffer = b
+    mode.spawned
     @name_map[realtitle] = b
 
     @buffers.unshift b
diff --git a/lib/sup/mode.rb b/lib/sup/mode.rb
@@ -37,6 +37,7 @@ class Mode
   def draw; end
   def focus; end
   def blur; end
+  def spawned; end
   def cancel_search!; end
   def in_search?; false end
   def status; ""; end
diff --git a/lib/sup/modes/contact_list_mode.rb b/lib/sup/modes/contact_list_mode.rb
@@ -83,7 +83,6 @@ class ContactListMode < LineCursorMode
   def multi_search people
     mode = PersonSearchResultsMode.new people
     BufferManager.spawn "search for #{people.map { |p| p.name }.join(', ')}", mode
-    mode.load_threads :num => mode.buffer.content_height
   end
 
   def search
diff --git a/lib/sup/modes/label_search_results_mode.rb b/lib/sup/modes/label_search_results_mode.rb
@@ -29,8 +29,7 @@ class LabelSearchResultsMode < ThreadIndexMode
     when :inbox
       BufferManager.raise_to_front InboxMode.instance.buffer
     else
-      b, new = BufferManager.spawn_unless_exists("All threads with label '#{label}'") { LabelSearchResultsMode.new [label] }
-      b.mode.load_threads :num => b.content_height if new
+      BufferManager.spawn_unless_exists("All threads with label '#{label}'") { LabelSearchResultsMode.new [label] }
     end
   end
 end
diff --git a/lib/sup/modes/line_cursor_mode.rb b/lib/sup/modes/line_cursor_mode.rb
@@ -31,6 +31,8 @@ class LineCursorMode < ScrollMode
     super opts
   end
 
+  def spawned; call_load_more_callbacks buffer.content_height; end
+
   def cleanup
     @load_more_thread.kill
     super
diff --git a/lib/sup/modes/search_results_mode.rb b/lib/sup/modes/search_results_mode.rb
@@ -49,7 +49,6 @@ class SearchResultsMode < ThreadIndexMode
       short_text = text.length < 20 ? text : text[0 ... 20] + "..."
       mode = SearchResultsMode.new query
       BufferManager.spawn "search: \"#{short_text}\"", mode
-      mode.load_threads :num => mode.buffer.content_height
     rescue Index::ParseError => e
       BufferManager.flash "Problem: #{e.message}!"
     end
diff --git a/lib/sup/modes/thread_view_mode.rb b/lib/sup/modes/thread_view_mode.rb
@@ -205,7 +205,7 @@ EOS
     @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
     update
   end
-  
+
   def reload
     update
   end
@@ -298,7 +298,6 @@ EOS
     p = @person_lines[curpos] or return
     mode = PersonSearchResultsMode.new [p]
     BufferManager.spawn "Search for #{p.name}", mode
-    mode.load_threads :num => mode.buffer.content_height
   end
 
   def compose
diff --git a/test/unit/test_line_cursor_mode.rb b/test/unit/test_line_cursor_mode.rb
@@ -12,8 +12,9 @@ class TestLineCursorMode < Minitest::Test
     }
     Redwood::BufferManager.init
     @modes_to_cleanup = []
-    @lines = (0...40).map { |i| "line #{i}" }
+    @lines = []
     @load_more = Thread::Queue.new
+    @buffer_height = 41  # 1 for status line, 40 usable lines
   end
 
   def teardown
@@ -29,7 +30,8 @@ class TestLineCursorMode < Minitest::Test
     mode.define_singleton_method(:lines) { lines.length }
     mode.define_singleton_method(:[]) { |i| lines[i] }
     mode.send(:to_load_more) { |n| @load_more << n }
-    mode.buffer = Redwood::DummyBuffer.new 100, lines.length + 1
+    mode.buffer = Redwood::DummyBuffer.new 100, @buffer_height
+    mode.spawned
     mode.draw
     mode
   end
@@ -44,11 +46,13 @@ class TestLineCursorMode < Minitest::Test
     end
     refute_nil requested, "Expected load_more callbacks to fire"
     assert_equal n, requested
-    (0...n).map { |i| @lines << "more line #{i}" }
+    (0...n).map { |i| @lines << "line #{i}" }
   end
 
   def test_cursor_down
     mode = make_mode
+    expect_load_more 40
+    mode.draw  # curpos gets messed up without this call, why?
 
     20.times do
       mode.handle_input Ncurses::CharCode.character('j')
@@ -75,6 +79,7 @@ class TestLineCursorMode < Minitest::Test
 
   def test_scroll_down
     mode = make_mode
+    expect_load_more 40
 
     ## When the cursor is already at the top, it moves with the scroll.
     assert_equal 0, mode.curpos
@@ -101,6 +106,7 @@ class TestLineCursorMode < Minitest::Test
 
   def test_page_down
     mode = make_mode
+    expect_load_more 40
 
     ## In theory, we should scroll a full page down. But because we always load
     ## exactly enough lines to fill one page, the first time we are off by one.
@@ -143,6 +149,7 @@ class TestLineCursorMode < Minitest::Test
 
   def test_page_down_when_fully_populated
     mode = make_mode
+    expect_load_more 40
     (0...120).map { |i| @lines << "more line #{i}" }  # enough for 4 full pages
 
     mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)