Skip to content

Add thread_select_unread setting to open threads at oldest unread message#125

Closed
klement wants to merge 1 commit into
akissinger:masterfrom
klement:thread-select-unread
Closed

Add thread_select_unread setting to open threads at oldest unread message#125
klement wants to merge 1 commit into
akissinger:masterfrom
klement:thread-select-unread

Conversation

@klement

@klement klement commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

When enabled, opening a thread selects the oldest unread matching message
instead of the oldest matching message. Falls back to the most recent message
if all messages are read. Also fixes a crash in get_last_msg_idx when called
with an invalid root index.

Co-Authored-By: Claude Opus 4.6 [email protected]

…sage

When enabled, opening a thread selects the oldest unread matching message
instead of the oldest matching message. Falls back to the most recent message
if all messages are read. Also fixes a crash in get_last_msg_idx when called
with an invalid root index.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

@laarmen laarmen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First off, please don't bundle fixes and features in a single commit, it makes the review harder. As a rule of thumb, if there's an "also does that" in your commit message, it's a solid hint it should have been split off 🙂 .

Secondly, adding a configuration flag for this feels wrong to me. The affected method becomes a lot harder to read and reason about, and I really doubt you'd want that behaviour for all searches. IMHO, there are other avenues to solve this that would be less intrusive and/or more broadly useful:

  • In your settings, override the thread opening keybinding to immediatly call next_unread. You can either dig through p.app to get the current panel (oddly enough we don't have any helper for that) or do some minimal scaffolding to propagate the panel, e.g. something like this:
diff --git a/dodo/app.py b/dodo/app.py
index 857d9fd..2e3082b 100644
--- a/dodo/app.py
+++ b/dodo/app.py
@@ -194,7 +194,7 @@ class Dodo(QApplication):
         p = search.SearchPanel(self, query, keep_open=keep_open)
         self.add_panel(p)
 
-    def open_thread(self, thread_id: str, query: str) -> None:
+    def open_thread(self, thread_id: str, query: str) -> thread.ThreadPanel:
         """Open a thread panel with the given thread_id
 
         If a panel with this thread_id is already open, switch to it rather than
@@ -204,10 +204,11 @@ class Dodo(QApplication):
             w = self.tabs.widget(i)
             if isinstance(w, thread.ThreadPanel) and (w.thread_id, w.query) == (thread_id, query):
                 self.tabs.setCurrentIndex(i)
-                return
+                return w
 
         p = thread.ThreadPanel(self, thread_id, query)
         self.add_panel(p)
+        return p
 
     def open_compose(self, mode: str='', msg: Optional[dict]=None) -> None:
         """Open a compose panel
diff --git a/dodo/search.py b/dodo/search.py
index bc69842..cb4df49 100644
--- a/dodo/search.py
+++ b/dodo/search.py
@@ -391,12 +391,12 @@ class SearchPanel(panel.Panel):
         pos = self.tree.rect().topLeft()
         self.tree.setCurrentIndex(self.tree.indexAt(pos))
 
-    def open_current_thread(self) -> None:
+    def open_current_thread(self) -> thread.ThreadPanel|None:
         """Open the selected thread"""
 
         thread_id = self.model.thread_id(self.tree.currentIndex())
         if thread_id:
-            self.app.open_thread(thread_id, self.model.q)
+            return self.app.open_thread(thread_id, self.model.q)
     
     def toggle_thread_tag(self, tag: str) -> None:
         """Toggle the given thread tag"""

Which allowed me to write this binding in my local config:

dodo.keymap.search_keymap['Q'] = ('open first unread', lambda p: p.open_current_thread().next_unread())
  • Add the ability to refine searches. If you had a method SearchPanel.refine_search(new_search: str), you could have a binding like this:
dodo.keymap.search_keymap['U'] = ('only unread', lambda p: p.refine_search(p.model.q + " and tag:unread"))

where opening a thread would automatically do what you want it to do.

There are other possible avenues, of course.

@klement

klement commented Mar 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for your feedback on this. Using your proposed change and keymap works well for threads containing unread messages, but has this weird behaviour when opening a fully read thread as it seems to land on a message somewhere in the middle.

Adjusting the keymap like this seems to fix that:

dodo.keymap.search_keymap['<enter>'] = ('open first unread', lambda p: (
      (t := p.open_current_thread()),
      t and t._select_index(
          u if (u := t.model.next_unread(t.current_index)).isValid()
          else t.model.get_last_msg_idx()
      )
  ))

I’m not familiar enough with the codebase to tell whether this reveals an underlying issue or works as intended though.

@laarmen

laarmen commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator

Assuming you are running my patch on top of master, this shouldn't be possible. App.add_panel() calls p.setFocus(), which will seed the data through a refresh() call, at which point you'll have the model fully fed and the current index set to whatever default_message() says.

laarmen pushed a commit to laarmen/dodo that referenced this pull request Mar 14, 2026
This needs to be done through a callback passed in the constructor, as
the thread selection only happens after the model has been refreshed for
the first time, which might be way after the creation of the panel
itself.

A real-life example of a need for this is someone wishing to open
threads directly on the first unread message rather than the first
search match, see akissinger#125
laarmen pushed a commit to laarmen/dodo that referenced this pull request Mar 14, 2026
This needs to be done through a callback passed in the constructor, as
the thread selection only happens after the model has been refreshed for
the first time, which might be way after the creation of the panel
itself.

A real-life example of a need for this is someone wishing to open
threads directly on the first unread message rather than the first
search match, see akissinger#125

Note that open_current_thread needs to have its new argument marked as
keyword-only as this method is connected to an event that provides
positional argument that we just don't care about.
@laarmen

laarmen commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator

@klement Please see #128, it should be better. Note that the approach for the binding is slightly different.

laarmen added a commit to laarmen/dodo that referenced this pull request Mar 15, 2026
This needs to be done through a callback passed in the constructor, as
the thread selection only happens after the model has been refreshed for
the first time, which might be way after the creation of the panel
itself.

A real-life example of a need for this is someone wishing to open
threads directly on the first unread message rather than the first
search match, see akissinger#125

Note that open_current_thread needs to have its new argument marked as
keyword-only as this method is connected to an event that provides
positional argument that we just don't care about.
laarmen added a commit to laarmen/dodo that referenced this pull request Mar 15, 2026
This needs to be done through a callback passed in the constructor, as
the thread selection only happens after the model has been refreshed for
the first time, which might be way after the creation of the panel
itself.

A real-life example of a need for this is someone wishing to open
threads directly on the first unread message rather than the first
search match, see akissinger#125

Note that open_current_thread needs to have its new argument marked as
keyword-only as this method is connected to an event that provides
positional argument that we just don't care about.
@klement klement closed this Mar 17, 2026
@klement
klement deleted the thread-select-unread branch March 17, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants