diff --git a/.coverage b/.coverage index 8ce7f7c..e11f01a 100644 Binary files a/.coverage and b/.coverage differ 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: