From 0ca266ce1cd01705d8e87bcc1b8f6d7317eb3760 Mon Sep 17 00:00:00 2001 From: Bendt Date: Wed, 7 Jan 2026 13:08:15 -0500 Subject: [PATCH] big improvements and bugs --- .coverage | Bin 69632 -> 69632 bytes src/mail/email_viewer.tcss | 51 +++++++++++++-- src/mail/notification_compressor.py | 2 +- src/mail/widgets/ContentContainer.py | 93 +++++++++++++++++++++------ src/services/dstask/client.py | 18 ++++-- src/tasks/app.py | 4 +- 6 files changed, 132 insertions(+), 36 deletions(-) diff --git a/.coverage b/.coverage index 8ce7f7c43c9938bde47a7aa0b96db77430cc0f69..e11f01a8c6b275cb9679e562b7081397d17ca264 100644 GIT binary patch delta 5190 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 diff --git a/src/mail/email_viewer.tcss b/src/mail/email_viewer.tcss index 128e132..b64d917 100644 --- a/src/mail/email_viewer.tcss +++ b/src/mail/email_viewer.tcss @@ -183,11 +183,30 @@ EnvelopeListItem.unread .email-subject { text-style: bold; } -/* Selected message styling */ +/* Selected/checked message styling (for multi-select) */ EnvelopeListItem.selected { tint: $accent 20%; } +/* Currently highlighted/focused item styling - more prominent */ +EnvelopeListItem.highlighted { + background: $primary-darken-1; +} + +EnvelopeListItem.highlighted .sender-name { + color: $text; + text-style: bold; +} + +EnvelopeListItem.highlighted .email-subject { + color: $text; + text-style: bold; +} + +EnvelopeListItem.highlighted .message-datetime { + color: $secondary-lighten-2; +} + /* GroupHeader - date group separator */ GroupHeader { height: 1; @@ -254,12 +273,30 @@ GroupHeader .group-header-label { background: $surface-darken-1; } - & > ListItem { - &.-highlight, .selection { - color: $block-cursor-blurred-foreground; - background: $block-cursor-blurred-background; - text-style: $block-cursor-blurred-text-style; - } + /* Currently highlighted/focused item - make it very visible */ + & > ListItem.-highlight { + background: $primary-darken-2; + color: $text; + text-style: bold; + } + + /* Highlighted item's child elements */ + & > ListItem.-highlight EnvelopeListItem { + tint: $primary 30%; + } + + & > ListItem.-highlight .sender-name { + color: $text; + text-style: bold; + } + + & > ListItem.-highlight .email-subject { + color: $text; + text-style: bold; + } + + & > ListItem.-highlight .message-datetime { + color: $secondary-lighten-2; } } diff --git a/src/mail/notification_compressor.py b/src/mail/notification_compressor.py index b8d50bd..26ac5de 100644 --- a/src/mail/notification_compressor.py +++ b/src/mail/notification_compressor.py @@ -126,7 +126,7 @@ class NotificationCompressor: lines.append("") lines.append(f"*From: {from_addr}*") lines.append( - "*This is a compressed notification view. Press `m` to toggle full view.*" + "*This is a compressed notification. Press `m` to see full email.*" ) return "\n".join(lines) diff --git a/src/mail/widgets/ContentContainer.py b/src/mail/widgets/ContentContainer.py index 7054ff6..d7f6cf5 100644 --- a/src/mail/widgets/ContentContainer.py +++ b/src/mail/widgets/ContentContainer.py @@ -309,6 +309,7 @@ class ContentContainer(Vertical): self.content = Markdown("", id="markdown_content") self.html_content = Static("", id="html_content", markup=False) self.current_content = None + self.current_raw_content = None # Store original uncompressed content self.current_message_id = None self.current_folder: str | None = None self.current_account: str | None = None @@ -316,6 +317,7 @@ class ContentContainer(Vertical): self.current_envelope: Optional[Dict[str, Any]] = None self.current_notification_type: Optional[NotificationType] = None self.is_compressed_view: bool = False + self.compression_enabled: bool = True # Toggle for compression on/off # Calendar invite state self.calendar_panel: Optional[CalendarInvitePanel] = None @@ -355,26 +357,59 @@ class ContentContainer(Vertical): def _update_mode_indicator(self) -> None: """Update the border subtitle to show current mode.""" - mode_label = "Markdown" if self.current_mode == "markdown" else "HTML/Text" - mode_icon = ( - "\ue73e" if self.current_mode == "markdown" else "\uf121" - ) # nf-md-language_markdown / nf-fa-code + if self.current_mode == "markdown": + if self.is_compressed_view: + mode_label = "Compressed" + mode_icon = "\uf066" # nf-fa-compress + else: + mode_label = "Markdown" + mode_icon = "\ue73e" # nf-md-language_markdown + else: + mode_label = "HTML/Text" + mode_icon = "\uf121" # nf-fa-code self.border_subtitle = f"{mode_icon} {mode_label}" async def action_toggle_mode(self): - """Toggle between markdown/compressed and HTML viewing modes. + """Toggle between viewing modes. - For notification emails: cycles between compressed view and HTML. + For notification emails: cycles compressed → full markdown → HTML → compressed For regular emails: cycles between markdown and HTML. """ - if self.current_mode == "html": - self.current_mode = "markdown" - else: - self.current_mode = "html" + # Check if this is a compressible notification email + is_notification = ( + self.compressor.mode != "off" + and self.current_envelope + and self.compressor.should_compress(self.current_envelope) + ) - # Reload the content if we have a message ID - # This will re-apply compression if applicable for markdown mode - if self.current_message_id: + if is_notification: + # Three-state cycle for notifications: compressed → full → html → compressed + if self.current_mode == "markdown" and self.compression_enabled: + # Currently compressed markdown → show full markdown + self.compression_enabled = False + # Don't change mode, just re-display with compression off + elif self.current_mode == "markdown" and not self.compression_enabled: + # Currently full markdown → switch to HTML + self.current_mode = "html" + else: + # Currently HTML → back to compressed markdown + self.current_mode = "markdown" + self.compression_enabled = True + else: + # Simple two-state toggle for regular emails + if self.current_mode == "html": + self.current_mode = "markdown" + else: + self.current_mode = "html" + + # Re-display content with new mode/compression settings + if self.current_raw_content is not None: + # Use cached raw content instead of re-fetching + self._update_content(self.current_raw_content) + self._apply_view_mode() + self._update_mode_indicator() + elif self.current_message_id: + # Fall back to re-fetching if no cached content self.display_content( self.current_message_id, folder=self.current_folder, @@ -507,6 +542,10 @@ class ContentContainer(Vertical): self.current_account = account self.current_envelope = envelope + # Reset compression state for new message (start with compression enabled) + self.compression_enabled = True + self.current_raw_content = None + # Update the header with envelope data if envelope: subject = envelope.get("subject", "") @@ -780,17 +819,27 @@ class ContentContainer(Vertical): # Strip headers from content (they're shown in EnvelopeHeader) content = self._strip_headers_from_content(content) - # Store the raw content for link extraction + # Store the raw content for link extraction and for toggle mode self.current_content = content + self.current_raw_content = content # Keep original for mode toggling - # Apply notification compression if enabled - if self.compressor.mode != "off" and self.current_envelope: + # Apply notification compression if enabled AND compression toggle is on + display_content = content + if ( + self.compressor.mode != "off" + and self.current_envelope + and self.compression_enabled + ): compressed_content, notif_type = self.compressor.compress( content, self.current_envelope ) self.current_notification_type = notif_type - content = compressed_content - self.is_compressed_view = True + if notif_type is not None: + # Only use compressed content if compression was actually applied + display_content = compressed_content + self.is_compressed_view = True + else: + self.is_compressed_view = False else: self.current_notification_type = None self.is_compressed_view = False @@ -803,11 +852,13 @@ class ContentContainer(Vertical): try: if self.current_mode == "markdown": # For markdown mode, use the Markdown widget - display_content = content + final_content = display_content if compress_urls and not self.is_compressed_view: # Don't compress URLs in notification summaries (they're already formatted) - display_content = compress_urls_in_content(content, max_url_len) - self.content.update(display_content) + final_content = compress_urls_in_content( + display_content, max_url_len + ) + self.content.update(final_content) else: # For HTML mode, use the Static widget with markup # First, try to extract the body content if it's HTML diff --git a/src/services/dstask/client.py b/src/services/dstask/client.py index 6cc95ef..22488d1 100644 --- a/src/services/dstask/client.py +++ b/src/services/dstask/client.py @@ -193,7 +193,12 @@ class DstaskClient(TaskBackend): due: Optional[datetime] = None, notes: Optional[str] = None, ) -> Task: - """Create a new task.""" + """Create a new task. + + Notes are added using dstask's / syntax during creation, where + everything after / becomes the note content. Each word must be + a separate argument for this to work. + """ args = ["add", summary] if project: @@ -210,6 +215,13 @@ class DstaskClient(TaskBackend): # dstask uses various date formats args.append(f"due:{due.strftime('%Y-%m-%d')}") + # Add notes using / syntax - each word must be a separate argument + # dstask interprets everything after "/" as note content + if notes: + args.append("/") + # Split notes into words to pass as separate arguments + args.extend(notes.split()) + result = self._run_command(args) if result.returncode != 0: @@ -221,10 +233,6 @@ class DstaskClient(TaskBackend): # Find task by summary (best effort) for task in reversed(tasks): if task.summary == summary: - # Add notes if provided - if notes: - self._run_command(["note", str(task.id), notes]) - task.notes = notes return task # Return a placeholder if we can't find it diff --git a/src/tasks/app.py b/src/tasks/app.py index de4adee..27a0b76 100644 --- a/src/tasks/app.py +++ b/src/tasks/app.py @@ -645,8 +645,8 @@ class TasksApp(App): from .screens.AddTaskScreen import AddTaskScreen from .widgets.AddTaskForm import TaskFormData - # Get project names for dropdown - project_names = [p.name for p in self.projects if p.name] + # Get project names for dropdown (use all_projects which is populated on mount) + project_names = [p.name for p in self.all_projects if p.name] def handle_task_created(data: TaskFormData | None) -> None: if data is None or not self.backend: