diff --git a/src/config/settings.py b/src/config/settings.py index 3f158b2..5feca7e 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -24,6 +24,7 @@ class SettingsManager: self.config_file = self.config_dir / "bifrost.conf" self.config = configparser.ConfigParser() self.load_settings() + self.migrate_settings() def _get_config_dir(self) -> Path: """Get XDG config directory""" @@ -67,19 +68,22 @@ class SettingsManager: self.config.set('audio', 'sound_pack', 'default') self.config.set('audio', 'volume', '100') - # Accessibility settings - self.config.add_section('accessibility') - self.config.set('accessibility', 'announce_thread_state', 'true') - self.config.set('accessibility', 'auto_expand_mentions', 'false') - self.config.set('accessibility', 'keyboard_navigation_wrap', 'true') - self.config.set('accessibility', 'focus_follows_mouse', 'false') - - # Interface settings + # Interface settings (formerly accessibility + interface) self.config.add_section('interface') + self.config.set('interface', 'page_step_size', '5') + self.config.set('interface', 'verbose_announcements', 'true') + self.config.set('interface', 'announce_thread_state', 'true') + self.config.set('interface', 'auto_expand_mentions', 'false') + self.config.set('interface', 'keyboard_navigation_wrap', 'true') + self.config.set('interface', 'focus_follows_mouse', 'false') self.config.set('interface', 'default_timeline', 'home') self.config.set('interface', 'show_timestamps', 'true') self.config.set('interface', 'compact_mode', 'false') + # Post composition settings + self.config.add_section('composition') + self.config.set('composition', 'default_content_type', 'text/plain') + # Desktop notification settings self.config.add_section('notifications') self.config.set('notifications', 'enabled', 'true') @@ -92,6 +96,36 @@ class SettingsManager: self.save_settings() + def migrate_settings(self): + """Migrate settings from old structure to new""" + # Migrate accessibility section to interface section + if self.config.has_section('accessibility'): + if not self.config.has_section('interface'): + self.config.add_section('interface') + + # Migrate accessibility settings to interface + for option in self.config.options('accessibility'): + value = self.config.get('accessibility', option) + self.config.set('interface', option, value) + + # Remove old accessibility section + self.config.remove_section('accessibility') + + # Ensure composition section exists + if not self.config.has_section('composition'): + self.config.add_section('composition') + self.config.set('composition', 'default_content_type', 'text/plain') + + # Ensure interface section has required settings + if not self.config.has_option('interface', 'page_step_size'): + self.config.set('interface', 'page_step_size', '5') + if not self.config.has_option('interface', 'verbose_announcements'): + self.config.set('interface', 'verbose_announcements', 'true') + if not self.config.has_option('interface', 'announce_thread_state'): + self.config.set('interface', 'announce_thread_state', 'true') + + self.save_settings() + def save_settings(self): """Save settings to config file""" with open(self.config_file, 'w') as f: diff --git a/src/main_window.py b/src/main_window.py index 8be9b85..80ffdf3 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -438,6 +438,7 @@ class MainWindow(QMainWindow): result = client.post_status( content=self.post_data['content'], visibility=self.post_data['visibility'], + content_type=self.post_data.get('content_type', 'text/plain'), content_warning=self.post_data['content_warning'], in_reply_to_id=self.post_data.get('in_reply_to_id'), poll=self.post_data.get('poll'), @@ -849,6 +850,7 @@ class MainWindow(QMainWindow): post.id, content=data['content'], visibility=data['visibility'], + content_type=data.get('content_type', 'text/plain'), content_warning=data['content_warning'] ) self.status_bar.showMessage("Post edited successfully", 2000) diff --git a/src/models/post.py b/src/models/post.py index 911073c..5a3db39 100644 --- a/src/models/post.py +++ b/src/models/post.py @@ -186,7 +186,7 @@ class Post: return self.account.display_name or self.account.username def get_content_text(self) -> str: - """Get plain text content, handling reblogs""" + """Get plain text content for editing (raw source), handling reblogs""" if self.reblog: return self.reblog.get_content_text() @@ -200,10 +200,27 @@ class Post: content = content.replace('<', '<').replace('>', '>').replace('&', '&') return content.strip() + def get_display_content(self) -> str: + """Get content for display (rendered HTML), handling reblogs""" + if self.reblog: + return self.reblog.get_display_content() + + # Always use the rendered HTML content for display + # This is what the server sends back after processing markdown/etc + if self.content: + # Basic HTML entity decoding + content = self.content.replace('<', '<').replace('>', '>').replace('&', '&') + # Strip HTML tags for plain text display in terminal-style interface + import re + content = re.sub(r'<[^>]+>', '', content) + return content.strip() + + return "" + def get_summary_for_screen_reader(self) -> str: """Get a summary suitable for screen reader announcement""" author = self.get_display_name() - content = self.get_content_text() + content = self.get_display_content() summary = f"{author}: {content}" diff --git a/src/widgets/compose_dialog.py b/src/widgets/compose_dialog.py index 77aefa5..984d10d 100644 --- a/src/widgets/compose_dialog.py +++ b/src/widgets/compose_dialog.py @@ -24,11 +24,12 @@ class PostThread(QThread): post_success = Signal(dict) # Emitted with post data on success post_failed = Signal(str) # Emitted with error message on failure - def __init__(self, account, content, visibility, content_warning=None, poll=None, media_ids=None): + def __init__(self, account, content, visibility, content_type='text/plain', content_warning=None, poll=None, media_ids=None): super().__init__() self.account = account self.content = content self.visibility = visibility + self.content_type = content_type self.content_warning = content_warning self.poll = poll self.media_ids = media_ids or [] @@ -41,6 +42,7 @@ class PostThread(QThread): result = client.post_status( content=self.content, visibility=self.visibility, + content_type=self.content_type, content_warning=self.content_warning, poll=self.poll, media_ids=self.media_ids @@ -65,6 +67,7 @@ class ComposeDialog(QDialog): self.media_upload_widget = None self.setup_ui() self.setup_shortcuts() + self.load_default_settings() def setup_ui(self): """Initialize the compose dialog UI""" @@ -94,10 +97,11 @@ class ComposeDialog(QDialog): options_group = QGroupBox("Post Options") options_layout = QVBoxLayout(options_group) - # Visibility settings - visibility_layout = QHBoxLayout() - visibility_layout.addWidget(QLabel("Visibility:")) + # Post settings row 1: Visibility and Content Type + settings_row1 = QHBoxLayout() + # Visibility settings + settings_row1.addWidget(QLabel("Visibility:")) self.visibility_combo = AccessibleComboBox() self.visibility_combo.setAccessibleName("Post Visibility") self.visibility_combo.addItems([ @@ -106,9 +110,19 @@ class ComposeDialog(QDialog): "Followers Only", "Direct Message" ]) - visibility_layout.addWidget(self.visibility_combo) - visibility_layout.addStretch() - options_layout.addLayout(visibility_layout) + settings_row1.addWidget(self.visibility_combo) + + settings_row1.addWidget(QLabel("Content Type:")) + self.content_type_combo = AccessibleComboBox() + self.content_type_combo.setAccessibleName("Content Type") + self.content_type_combo.setAccessibleDescription("Choose the format for this post") + self.content_type_combo.addItem("Plain Text", "text/plain") + self.content_type_combo.addItem("Markdown", "text/markdown") + self.content_type_combo.addItem("HTML", "text/html") + settings_row1.addWidget(self.content_type_combo) + + settings_row1.addStretch() + options_layout.addLayout(settings_row1) # Content warnings self.cw_checkbox = QCheckBox("Add Content Warning") @@ -235,6 +249,14 @@ class ComposeDialog(QDialog): cancel_shortcut = QShortcut(QKeySequence.Cancel, self) cancel_shortcut.activated.connect(self.reject) + def load_default_settings(self): + """Load default settings from configuration""" + # Set default content type + default_type = self.settings.get('composition', 'default_content_type', 'text/plain') + index = self.content_type_combo.findData(default_type) + if index >= 0: + self.content_type_combo.setCurrentIndex(index) + def toggle_content_warning(self, enabled: bool): """Toggle content warning field visibility""" if enabled: @@ -295,6 +317,9 @@ class ComposeDialog(QDialog): } visibility = visibility_map.get(visibility_text, "public") + # Get content type + content_type = self.content_type_combo.currentData() + content_warning = None if self.cw_checkbox.isChecked(): content_warning = self.cw_edit.toPlainText().strip() @@ -335,6 +360,7 @@ class ComposeDialog(QDialog): 'account': active_account, 'content': content, 'visibility': visibility, + 'content_type': content_type, 'content_warning': content_warning, 'poll': poll_data, 'media_ids': media_ids diff --git a/src/widgets/settings_dialog.py b/src/widgets/settings_dialog.py index f74ad0c..2fe6b36 100644 --- a/src/widgets/settings_dialog.py +++ b/src/widgets/settings_dialog.py @@ -43,8 +43,8 @@ class SettingsDialog(QDialog): # Desktop notifications tab self.setup_notifications_tab() - # Accessibility settings tab - self.setup_accessibility_tab() + # Interface settings tab + self.setup_interface_tab() # Button box button_box = QDialogButtonBox( @@ -149,10 +149,10 @@ class SettingsDialog(QDialog): self.tabs.addTab(notifications_widget, "&Notifications") - def setup_accessibility_tab(self): - """Set up the accessibility settings tab""" - accessibility_widget = QWidget() - layout = QVBoxLayout(accessibility_widget) + def setup_interface_tab(self): + """Set up the interface settings tab""" + interface_widget = QWidget() + layout = QVBoxLayout(interface_widget) # Navigation settings nav_group = QGroupBox("Navigation Settings") @@ -166,6 +166,20 @@ class SettingsDialog(QDialog): layout.addWidget(nav_group) + # Post composition settings + post_group = QGroupBox("Post Composition") + post_layout = QFormLayout(post_group) + + self.default_content_type = AccessibleComboBox() + self.default_content_type.setAccessibleName("Default Content Type") + self.default_content_type.setAccessibleDescription("Default format for new posts") + self.default_content_type.addItem("Plain Text", "text/plain") + self.default_content_type.addItem("Markdown", "text/markdown") + self.default_content_type.addItem("HTML", "text/html") + post_layout.addRow("Default content type:", self.default_content_type) + + layout.addWidget(post_group) + # Screen reader options sr_group = QGroupBox("Screen Reader Options") sr_layout = QVBoxLayout(sr_group) @@ -195,7 +209,7 @@ class SettingsDialog(QDialog): layout.addWidget(timeline_group) layout.addStretch() - self.tabs.addTab(accessibility_widget, "A&ccessibility") + self.tabs.addTab(interface_widget, "&Interface") def load_sound_packs(self): """Load available sound packs from the sounds directory""" @@ -250,10 +264,16 @@ class SettingsDialog(QDialog): self.notify_follows.setChecked(self.settings.get_bool('notifications', 'follows', True)) self.notify_timeline_updates.setChecked(self.settings.get_bool('notifications', 'timeline_updates', False)) - # Accessibility settings - self.page_step_size.setValue(int(self.settings.get('accessibility', 'page_step_size', 5) or 5)) - self.verbose_announcements.setChecked(self.settings.get_bool('accessibility', 'verbose_announcements', True)) - self.announce_thread_state.setChecked(self.settings.get_bool('accessibility', 'announce_thread_state', True)) + # Interface settings + self.page_step_size.setValue(int(self.settings.get('interface', 'page_step_size', 5) or 5)) + self.verbose_announcements.setChecked(self.settings.get_bool('interface', 'verbose_announcements', True)) + self.announce_thread_state.setChecked(self.settings.get_bool('interface', 'announce_thread_state', True)) + + # Post composition settings + default_type = self.settings.get('composition', 'default_content_type', 'text/plain') + index = self.default_content_type.findData(default_type) + if index >= 0: + self.default_content_type.setCurrentIndex(index) # Timeline settings self.posts_per_page.setValue(int(self.settings.get('timeline', 'posts_per_page', 40) or 40)) @@ -275,10 +295,14 @@ class SettingsDialog(QDialog): self.settings.set('notifications', 'follows', self.notify_follows.isChecked()) self.settings.set('notifications', 'timeline_updates', self.notify_timeline_updates.isChecked()) - # Accessibility settings - self.settings.set('accessibility', 'page_step_size', self.page_step_size.value()) - self.settings.set('accessibility', 'verbose_announcements', self.verbose_announcements.isChecked()) - self.settings.set('accessibility', 'announce_thread_state', self.announce_thread_state.isChecked()) + # Interface settings + self.settings.set('interface', 'page_step_size', self.page_step_size.value()) + self.settings.set('interface', 'verbose_announcements', self.verbose_announcements.isChecked()) + self.settings.set('interface', 'announce_thread_state', self.announce_thread_state.isChecked()) + + # Post composition settings + selected_content_type = self.default_content_type.currentData() + self.settings.set('composition', 'default_content_type', selected_content_type) # Timeline settings self.settings.set('timeline', 'posts_per_page', self.posts_per_page.value()) diff --git a/src/widgets/timeline_view.py b/src/widgets/timeline_view.py index 3327d80..f2e4665 100644 --- a/src/widgets/timeline_view.py +++ b/src/widgets/timeline_view.py @@ -184,7 +184,7 @@ class TimelineView(AccessibleTreeWidget): # Show desktop notification (skip if this is initial load) if not self.skip_notifications: - content_preview = post.get_content_text() + content_preview = post.get_display_content() if notification_type == 'mention': self.notification_manager.notify_mention(sender, content_preview) @@ -448,7 +448,7 @@ class TimelineView(AccessibleTreeWidget): return # Get the full post content - content = post.get_content_text() + content = post.get_display_content() author = post.get_display_name() # Format for clipboard @@ -471,7 +471,7 @@ class TimelineView(AccessibleTreeWidget): if not post: return [] - content = post.get_content_text() + content = post.get_display_content() # URL regex pattern - matches http/https URLs, more comprehensive url_pattern = r'https?://[^\s<>"\'`\)\]\}]+'