git.lucas.co / cce-mail
mail client (IMAP/SMTP)
git clone https://git.lucas.co/cce-mail.git

commit8eb5c0f5c3ea6c8585f1ed1bf4bb73fbaaa3df94
parente097879f33
authorLucas Galante <[email protected]>
date2026-07-23 10:15
fix: register rebuilt list-row buttons immediately + tokio reactor for sync threads

Two live-verified fixes:

1. display_list registers dispatch roots at the top of the frame, then
   rebuilds email_buttons when the row count changes — the fresh WidgetIds
   were never registered, so every click/hover on the list was dropped as a
   stale root and (since dropped events set no rebuild flag) nothing ever
   re-registered them. Switching Inbox->Accounts made the whole account
   list permanently dead, which also meant SelectAccount could never fire
   and a real Gmail sync never started. Re-register rows right after the
   rebuild.

2. sync_imap/send_smtp worker threads drove reqwest futures with
   pollster::block_on, but hyper needs a live tokio reactor and the
   rt.enter() guard in main() only covers the main thread — the OAuth
   refresh panicked the worker with 'there is no reactor running'. Give
   workers a current-thread runtime via block_on_worker().

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/main.rs | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 0b85df6..0aa1bef 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -478,6 +478,17 @@ impl imap::Authenticator for ImapOAuth2 {
     }
 }
 
+/// Block on a future from a plain worker thread. `pollster::block_on` is NOT
+/// enough for reqwest/hyper futures — they need a live tokio reactor, and the
+/// `rt.enter()` guard in main() only covers the main thread.
+fn block_on_worker<F: std::future::Future>(fut: F) -> Result<F::Output, String> {
+    tokio::runtime::Builder::new_current_thread()
+        .enable_all()
+        .build()
+        .map_err(|e| format!("tokio runtime: {}", e))
+        .map(|rt| rt.block_on(fut))
+}
+
 fn sync_imap(mut account: AccountInfo, sender: calloop::channel::Sender<AppMessage>) {
     std::thread::spawn(move || {
         // Skip connecting for mock credentials
@@ -488,7 +499,7 @@ fn sync_imap(mut account: AccountInfo, sender: calloop::channel::Sender<AppMessa
         let mut access_token = account.password.clone();
         if account.is_oauth {
             let mut acc = account.clone();
-            match pollster::block_on(refresh_access_token(&mut acc)) {
+            match block_on_worker(refresh_access_token(&mut acc)).and_then(|r| r) {
                 Ok(token) => {
                     access_token = token;
                     // Send refreshed tokens back to main thread to save them
@@ -651,7 +662,7 @@ fn send_smtp(mut account: AccountInfo, to: String, subject: String, body: String
         let mut access_token = account.password.clone();
         if account.is_oauth {
             let mut acc = account.clone();
-            match pollster::block_on(refresh_access_token(&mut acc)) {
+            match block_on_worker(refresh_access_token(&mut acc)).and_then(|r| r) {
                 Ok(token) => {
                     access_token = token;
                     let _ = sender.send(AppMessage::UpdateAccountTokens(
@@ -1712,6 +1723,14 @@ impl Application for ClearEmailApp {
                 self.email_buttons = (0..list_count)
                     .map(|_| Button::new_list_row(0.0, 0.0, 0.0, 0.0))
                     .collect();
+                // register_dispatch_roots() already ran this frame with the OLD
+                // buttons; the fresh ids must be registered now or every click on
+                // the list is dropped as a stale root (and nothing re-triggers a
+                // rebuild, so the list stays dead).
+                for btn in self.email_buttons.iter_mut() {
+                    let (id, ptr) = (btn.id(), btn.as_ptr_mut());
+                    self.ui_context.register_widget(id, ptr);
+                }
             }
 
             // Get selected email state to avoid borrowing self while mutating