13 | A simple, opinionated plain text editor designed for writers, journalists and researchers. Jottr has features like distraction-free writing, an integrated web-browser, customizable search in specific sites from the context menu and smart auto-completion. 14 |
15 |Bug fixes and improvements
29 |Context menu reordered
30 |Published: {entry.published}
" 198 | if hasattr(entry, 'description'): 199 | content += f"{entry.description}
" 200 | if hasattr(entry, 'link'): 201 | content += f'' 202 | self.content_viewer.setHtml(content) 203 | 204 | def manage_feeds(self): 205 | dialog = FeedManagerDialog(self.feeds, self) 206 | if dialog.exec_() == QDialog.Accepted: 207 | self.feeds = dialog.get_feeds() 208 | self.save_feeds() 209 | self.update_feed_selector() 210 | self.refresh_current_feed() -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/rss_tab.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QWidget, QVBoxLayout 2 | from rss_reader import RSSReader 3 | 4 | class RSSTab(QWidget): 5 | def __init__(self, parent=None): 6 | super().__init__(parent) 7 | layout = QVBoxLayout(self) 8 | self.rss_reader = RSSReader() 9 | layout.addWidget(self.rss_reader) -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/settings_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, 2 | QLineEdit, QPushButton, QListWidget, QTabWidget, 3 | QWidget, QCheckBox, QMessageBox, QInputDialog) 4 | from PyQt5.QtCore import Qt 5 | import json 6 | import os 7 | 8 | class SettingsDialog(QDialog): 9 | def __init__(self, settings_manager, parent=None): 10 | super().__init__(parent) 11 | self.settings_manager = settings_manager 12 | self.setWindowTitle("Settings") 13 | self.setMinimumWidth(500) 14 | 15 | self.setup_ui() 16 | 17 | def setup_ui(self): 18 | """Setup the UI components""" 19 | # Create layout 20 | layout = QVBoxLayout(self) 21 | 22 | # Create tab widget 23 | tabs = QTabWidget() 24 | 25 | # Browser tab 26 | browser_tab = QWidget() 27 | browser_layout = QVBoxLayout(browser_tab) 28 | 29 | # Homepage setting 30 | homepage_layout = QHBoxLayout() 31 | homepage_label = QLabel("Homepage:") 32 | self.homepage_edit = QLineEdit() 33 | self.homepage_edit.setText(self.settings_manager.get_setting('homepage', 'https://www.apnews.com/')) 34 | homepage_layout.addWidget(homepage_label) 35 | homepage_layout.addWidget(self.homepage_edit) 36 | browser_layout.addLayout(homepage_layout) 37 | 38 | # Search sites 39 | search_label = QLabel("Site-specific searches:") 40 | browser_layout.addWidget(search_label) 41 | 42 | self.search_list = QListWidget() 43 | self.load_search_sites() 44 | browser_layout.addWidget(self.search_list) 45 | 46 | # Search site buttons 47 | search_buttons = QHBoxLayout() 48 | add_search = QPushButton("Add") 49 | edit_search = QPushButton("Edit") 50 | delete_search = QPushButton("Delete") 51 | add_search.clicked.connect(self.add_search_site) 52 | edit_search.clicked.connect(self.edit_search_site) 53 | delete_search.clicked.connect(self.delete_search_site) 54 | search_buttons.addWidget(add_search) 55 | search_buttons.addWidget(edit_search) 56 | search_buttons.addWidget(delete_search) 57 | browser_layout.addLayout(search_buttons) 58 | 59 | # Dictionary tab 60 | dict_tab = QWidget() 61 | dict_layout = QVBoxLayout(dict_tab) 62 | 63 | dict_label = QLabel("User Dictionary:") 64 | dict_layout.addWidget(dict_label) 65 | 66 | self.dict_list = QListWidget() 67 | self.load_user_dict() 68 | dict_layout.addWidget(self.dict_list) 69 | 70 | # Dictionary buttons 71 | dict_buttons = QHBoxLayout() 72 | add_word = QPushButton("Add Word") 73 | delete_word = QPushButton("Delete Word") 74 | add_word.clicked.connect(self.add_dict_word) 75 | delete_word.clicked.connect(self.delete_dict_word) 76 | dict_buttons.addWidget(add_word) 77 | dict_buttons.addWidget(delete_word) 78 | dict_layout.addLayout(dict_buttons) 79 | 80 | # Add tabs 81 | tabs.addTab(browser_tab, "Browser") 82 | tabs.addTab(dict_tab, "Dictionary") 83 | 84 | layout.addWidget(tabs) 85 | 86 | # Dialog buttons 87 | buttons = QHBoxLayout() 88 | ok_button = QPushButton("OK") 89 | cancel_button = QPushButton("Cancel") 90 | ok_button.clicked.connect(self.accept) 91 | cancel_button.clicked.connect(self.reject) 92 | buttons.addWidget(ok_button) 93 | buttons.addWidget(cancel_button) 94 | layout.addLayout(buttons) 95 | 96 | def load_search_sites(self): 97 | """Load search sites from settings""" 98 | sites = self.settings_manager.get_setting('search_sites', { 99 | 'AP News': 'site:apnews.com', 100 | 'Reuters': 'site:reuters.com', 101 | 'BBC News': 'site:bbc.com/news' 102 | }) 103 | for name, site in sites.items(): 104 | self.search_list.addItem(f"{name}: {site}") 105 | 106 | def load_user_dict(self): 107 | """Load user dictionary words""" 108 | words = self.settings_manager.get_setting('user_dictionary', []) 109 | self.dict_list.addItems(words) 110 | 111 | def add_search_site(self): 112 | """Add new search site""" 113 | dialog = SearchSiteDialog(self) 114 | if dialog.exec_(): 115 | name, site = dialog.get_data() 116 | self.search_list.addItem(f"{name}: {site}") 117 | 118 | def edit_search_site(self): 119 | """Edit selected search site""" 120 | current = self.search_list.currentItem() 121 | if current: 122 | name, site = current.text().split(': ', 1) 123 | dialog = SearchSiteDialog(self, name, site) 124 | if dialog.exec_(): 125 | new_name, new_site = dialog.get_data() 126 | current.setText(f"{new_name}: {new_site}") 127 | 128 | def delete_search_site(self): 129 | """Delete selected search site""" 130 | current = self.search_list.currentRow() 131 | if current >= 0: 132 | self.search_list.takeItem(current) 133 | 134 | def add_dict_word(self): 135 | """Add word to user dictionary""" 136 | word, ok = QInputDialog.getText(self, "Add Word", "Enter word:") 137 | if ok and word: 138 | self.dict_list.addItem(word) 139 | 140 | def delete_dict_word(self): 141 | """Delete word from user dictionary""" 142 | current = self.dict_list.currentRow() 143 | if current >= 0: 144 | self.dict_list.takeItem(current) 145 | 146 | def get_data(self): 147 | """Get dialog data""" 148 | return { 149 | 'homepage': self.homepage_edit.text(), 150 | 'search_sites': self.get_search_sites(), 151 | 'user_dictionary': self.get_user_dictionary() 152 | } 153 | 154 | def get_search_sites(self): 155 | """Get search sites from list widget""" 156 | sites = {} 157 | for i in range(self.search_list.count()): 158 | name, site = self.search_list.item(i).text().split(': ', 1) 159 | sites[name] = site 160 | return sites 161 | 162 | def get_user_dictionary(self): 163 | """Get words from dictionary list widget""" 164 | words = [] 165 | for i in range(self.dict_list.count()): 166 | words.append(self.dict_list.item(i).text()) 167 | return words 168 | 169 | class SearchSiteDialog(QDialog): 170 | def __init__(self, parent=None, name='', site=''): 171 | super().__init__(parent) 172 | self.setWindowTitle("Search Site") 173 | 174 | layout = QVBoxLayout(self) 175 | 176 | # Name field 177 | name_layout = QHBoxLayout() 178 | name_label = QLabel("Name:") 179 | self.name_edit = QLineEdit(name) 180 | name_layout.addWidget(name_label) 181 | name_layout.addWidget(self.name_edit) 182 | layout.addLayout(name_layout) 183 | 184 | # Site field 185 | site_layout = QHBoxLayout() 186 | site_label = QLabel("Site:") 187 | self.site_edit = QLineEdit(site) 188 | site_layout.addWidget(site_label) 189 | site_layout.addWidget(self.site_edit) 190 | layout.addLayout(site_layout) 191 | 192 | # Buttons 193 | buttons = QHBoxLayout() 194 | ok_button = QPushButton("OK") 195 | cancel_button = QPushButton("Cancel") 196 | ok_button.clicked.connect(self.accept) 197 | cancel_button.clicked.connect(self.reject) 198 | buttons.addWidget(ok_button) 199 | buttons.addWidget(cancel_button) 200 | layout.addLayout(buttons) 201 | 202 | def get_data(self): 203 | """Get dialog data""" 204 | return self.name_edit.text(), self.site_edit.text() -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/settings_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from PyQt5.QtGui import QFont 4 | import time 5 | 6 | class SettingsManager: 7 | def __init__(self): 8 | self.settings_file = os.path.join(os.path.expanduser("~"), ".editor_settings.json") 9 | self.settings = self.load_settings() 10 | 11 | # Create autosave directory 12 | self.autosave_dir = os.path.join(os.path.expanduser("~"), ".ap_editor_autosave") 13 | os.makedirs(self.autosave_dir, exist_ok=True) 14 | 15 | # Create running flag file 16 | self.running_flag = os.path.join(self.autosave_dir, "editor_running") 17 | # Set running flag 18 | with open(self.running_flag, 'w') as f: 19 | f.write(str(os.getpid())) 20 | 21 | # Setup backup and recovery directories 22 | self.backup_dir = os.path.join(os.path.expanduser("~"), ".ap_editor", "backups") 23 | self.recovery_dir = os.path.join(os.path.expanduser("~"), ".ap_editor", "recovery") 24 | os.makedirs(self.backup_dir, exist_ok=True) 25 | os.makedirs(self.recovery_dir, exist_ok=True) 26 | 27 | # Create session file 28 | self.session_file = os.path.join(self.recovery_dir, "session.json") 29 | self.create_session_file() 30 | 31 | # Create session state file 32 | self.session_state_file = os.path.join(self.recovery_dir, "session_state.json") 33 | 34 | # Initialize with unclean state 35 | self.initialize_session_state() 36 | 37 | # Clean up old session files if last exit was clean 38 | if os.path.exists(self.session_state_file): 39 | try: 40 | with open(self.session_state_file, 'r') as f: 41 | state = json.load(f) 42 | if state.get('clean_exit', False): 43 | self.cleanup_old_sessions() 44 | except: 45 | pass 46 | 47 | def load_settings(self): 48 | """Load all settings with defaults""" 49 | default_settings = { 50 | "theme": "Light", 51 | "font_family": "Consolas" if os.name == 'nt' else "DejaVu Sans Mono", 52 | "font_size": 10, 53 | "font_weight": 50, 54 | "font_italic": False, 55 | "show_snippets": True, 56 | "show_browser": True, 57 | "last_files": [], 58 | "homepage": "https://www.apnews.com/", 59 | "search_sites": { 60 | "AP News": "site:apnews.com", 61 | "Reuters": "site:reuters.com", 62 | "BBC News": "site:bbc.com/news" 63 | }, 64 | "user_dictionary": [], 65 | "start_focus_mode": False, 66 | "pane_states": { 67 | "snippets_visible": False, 68 | "browser_visible": False, 69 | "sizes": [700, 300, 300] 70 | } 71 | } 72 | 73 | try: 74 | if os.path.exists(self.settings_file): 75 | with open(self.settings_file, 'r') as f: 76 | saved_settings = json.load(f) 77 | # Merge saved settings with defaults 78 | return {**default_settings, **saved_settings} 79 | except Exception as e: 80 | print(f"Failed to load settings: {str(e)}") 81 | 82 | return default_settings 83 | 84 | def save_settings(self): 85 | with open(self.settings_file, 'w') as f: 86 | json.dump(self.settings, f) 87 | 88 | def get_font(self): 89 | font = QFont( 90 | self.settings["font_family"], 91 | self.settings["font_size"], 92 | self.settings["font_weight"] 93 | ) 94 | font.setItalic(self.settings["font_italic"]) 95 | return font 96 | 97 | def save_font(self, font): 98 | self.settings.update({ 99 | "font_family": font.family(), 100 | "font_size": font.pointSize(), 101 | "font_weight": font.weight(), 102 | "font_italic": font.italic() 103 | }) 104 | self.save_settings() 105 | 106 | def get_theme(self): 107 | return self.settings["theme"] 108 | 109 | def save_theme(self, theme): 110 | self.settings["theme"] = theme 111 | self.save_settings() 112 | 113 | def get_pane_visibility(self): 114 | return (self.settings["show_snippets"], self.settings["show_browser"]) 115 | 116 | def save_pane_visibility(self, show_snippets, show_browser): 117 | self.settings.update({ 118 | "show_snippets": show_snippets, 119 | "show_browser": show_browser 120 | }) 121 | self.save_settings() 122 | 123 | def save_last_files(self, files): 124 | """Save list of last opened files""" 125 | self.settings["last_files"] = files 126 | self.save_settings() 127 | 128 | def get_last_files(self): 129 | """Get list of last opened files""" 130 | return self.settings.get("last_files", []) 131 | 132 | def get_autosave_dir(self): 133 | """Get the directory for autosave files""" 134 | return self.autosave_dir 135 | 136 | def cleanup_autosave_dir(self): 137 | """Clean up old autosave files""" 138 | if os.path.exists(self.autosave_dir): 139 | try: 140 | # Remove files older than 7 days 141 | for filename in os.listdir(self.autosave_dir): 142 | filepath = os.path.join(self.autosave_dir, filename) 143 | if os.path.getmtime(filepath) < time.time() - 7 * 86400: 144 | os.remove(filepath) 145 | except: 146 | pass 147 | 148 | def clear_running_flag(self): 149 | """Clear the running flag on clean exit""" 150 | try: 151 | if os.path.exists(self.running_flag): 152 | os.remove(self.running_flag) 153 | except: 154 | pass 155 | 156 | def was_previous_crash(self): 157 | """Check if previous session crashed""" 158 | if os.path.exists(self.running_flag): 159 | try: 160 | with open(self.running_flag, 'r') as f: 161 | old_pid = int(f.read().strip()) 162 | # Check if the process is still running 163 | try: 164 | os.kill(old_pid, 0) 165 | # If we get here, the process is still running 166 | return False 167 | except OSError: 168 | # Process is not running, was a crash 169 | return True 170 | except: 171 | return True 172 | return False 173 | 174 | def create_session_file(self): 175 | """Create a session file to track clean/dirty exits""" 176 | session_data = { 177 | 'pid': os.getpid(), 178 | 'timestamp': time.time(), 179 | 'clean_exit': False 180 | } 181 | try: 182 | with open(self.session_file, 'w') as f: 183 | json.dump(session_data, f) 184 | except Exception as e: 185 | print(f"Failed to create session file: {str(e)}") 186 | 187 | def mark_clean_exit(self): 188 | """Mark that the editor exited cleanly""" 189 | try: 190 | if os.path.exists(self.session_file): 191 | with open(self.session_file, 'r') as f: 192 | session_data = json.load(f) 193 | session_data['clean_exit'] = True 194 | with open(self.session_file, 'w') as f: 195 | json.dump(session_data, f) 196 | except Exception as e: 197 | print(f"Failed to mark clean exit: {str(e)}") 198 | 199 | def needs_recovery(self): 200 | """Check if we need to recover from a crash""" 201 | try: 202 | if os.path.exists(self.session_file): 203 | with open(self.session_file, 'r') as f: 204 | session_data = json.load(f) 205 | return not session_data.get('clean_exit', True) 206 | return True # If no session file, assume we need recovery 207 | except Exception as e: 208 | print(f"Error checking recovery status: {str(e)}") 209 | return True # If we can't read the session file, assume we need recovery 210 | 211 | def get_backup_dir(self): 212 | return self.backup_dir 213 | 214 | def get_recovery_dir(self): 215 | return self.recovery_dir 216 | 217 | def initialize_session_state(self): 218 | """Initialize or update session state""" 219 | try: 220 | state = { 221 | 'clean_exit': False, 222 | 'timestamp': time.time(), 223 | 'open_tabs': [] 224 | } 225 | with open(self.session_state_file, 'w') as f: 226 | json.dump(state, f) 227 | except Exception as e: 228 | print(f"Failed to initialize session state: {str(e)}") 229 | 230 | def save_session_state(self, tab_ids, clean_exit=False): 231 | """Save the list of currently open tab IDs""" 232 | try: 233 | state = { 234 | 'open_tabs': tab_ids, 235 | 'clean_exit': clean_exit, 236 | 'timestamp': time.time() 237 | } 238 | with open(self.session_state_file, 'w') as f: 239 | json.dump(state, f) 240 | except Exception as e: 241 | print(f"Failed to save session state: {str(e)}") 242 | 243 | def get_session_state(self): 244 | """Get list of tab IDs that were open in last session""" 245 | try: 246 | if os.path.exists(self.session_state_file): 247 | with open(self.session_state_file, 'r') as f: 248 | state = json.load(f) 249 | return state.get('open_tabs', []) 250 | except Exception as e: 251 | print(f"Failed to load session state: {str(e)}") 252 | return [] 253 | 254 | def cleanup_old_sessions(self): 255 | """Clean up session files from previous clean exits""" 256 | # Don't clean up by default - let the session restore handle it 257 | pass 258 | 259 | def get_setting(self, key, default=None): 260 | """Get a setting value with a default fallback""" 261 | try: 262 | settings = self.load_settings() 263 | return settings.get(key, default) 264 | except Exception as e: 265 | print(f"Failed to get setting {key}: {str(e)}") 266 | return default 267 | 268 | def save_setting(self, key, value): 269 | """Save a single setting""" 270 | try: 271 | settings = self.load_settings() 272 | settings[key] = value 273 | with open(self.settings_file, 'w') as f: 274 | json.dump(settings, f) 275 | except Exception as e: 276 | print(f"Failed to save setting {key}: {str(e)}") -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/snippet_editor_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, 2 | QTextEdit, QPushButton, QLabel) 3 | 4 | class SnippetEditorDialog(QDialog): 5 | def __init__(self, title="", content="", parent=None): 6 | super().__init__(parent) 7 | self.setWindowTitle("Edit Snippet") 8 | self.setMinimumWidth(500) 9 | self.setMinimumHeight(400) 10 | 11 | layout = QVBoxLayout(self) 12 | 13 | # Title input 14 | title_layout = QHBoxLayout() 15 | title_label = QLabel("Title:") 16 | self.title_edit = QLineEdit(title) 17 | title_layout.addWidget(title_label) 18 | title_layout.addWidget(self.title_edit) 19 | layout.addLayout(title_layout) 20 | 21 | # Content input 22 | content_label = QLabel("Content:") 23 | layout.addWidget(content_label) 24 | self.content_edit = QTextEdit() 25 | self.content_edit.setPlainText(content) 26 | layout.addWidget(self.content_edit) 27 | 28 | # Buttons 29 | button_layout = QHBoxLayout() 30 | save_button = QPushButton("Save") 31 | cancel_button = QPushButton("Cancel") 32 | button_layout.addWidget(save_button) 33 | button_layout.addWidget(cancel_button) 34 | layout.addLayout(button_layout) 35 | 36 | save_button.clicked.connect(self.accept) 37 | cancel_button.clicked.connect(self.reject) 38 | 39 | def get_data(self): 40 | return { 41 | 'title': self.title_edit.text(), 42 | 'content': self.content_edit.toPlainText() 43 | } -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/snippet_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | class SnippetManager: 5 | def __init__(self): 6 | self.snippets = {} 7 | self.file_path = "snippets.json" 8 | self.load_snippets() 9 | 10 | def load_snippets(self): 11 | if os.path.exists(self.file_path): 12 | try: 13 | with open(self.file_path, 'r') as file: 14 | self.snippets = json.load(file) 15 | except: 16 | self.snippets = {} 17 | 18 | def save_snippets(self): 19 | with open(self.file_path, 'w') as file: 20 | json.dump(self.snippets, file) 21 | 22 | def add_snippet(self, title, text): 23 | self.snippets[title] = text 24 | self.save_snippets() 25 | 26 | def get_snippet(self, title): 27 | return self.snippets.get(title) 28 | 29 | def get_snippets(self): 30 | return list(self.snippets.keys()) 31 | 32 | def delete_snippet(self, title): 33 | if title in self.snippets: 34 | del self.snippets[title] 35 | self.save_snippets() 36 | 37 | def get_all_snippet_contents(self): 38 | """Return a list of all snippet contents""" 39 | return list(self.snippets.values()) -------------------------------------------------------------------------------- /packaging/debian/jottr/usr/share/jottr/theme_manager.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtGui import QColor, QPalette 2 | from PyQt5.QtWidgets import QStyleFactory 3 | 4 | class ThemeManager: 5 | @staticmethod 6 | def get_themes(): 7 | return { 8 | "Light": { 9 | "bg": "#ffffff", 10 | "text": "#000000", 11 | "selection": "#b3d4fc" 12 | }, 13 | "Dark": { 14 | "bg": "#1e1e1e", 15 | "text": "#d4d4d4", 16 | "selection": "#264f78" 17 | }, 18 | "Sepia": { 19 | "bg": "#f4ecd8", 20 | "text": "#5b4636", 21 | "selection": "#c4b5a0" 22 | } 23 | } 24 | 25 | @staticmethod 26 | def apply_theme(editor, theme_name): 27 | themes = ThemeManager.get_themes() 28 | if theme_name in themes: 29 | theme = themes[theme_name] 30 | editor.setStyleSheet(f""" 31 | QTextEdit {{ 32 | background-color: {theme['bg']}; 33 | color: {theme['text']}; 34 | selection-background-color: {theme['selection']}; 35 | font-family: {editor.font().family()}; 36 | font-size: {editor.font().pointSize()}pt; 37 | }} 38 | """) -------------------------------------------------------------------------------- /packaging/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | 6 | # Override all Python-related build steps 7 | override_dh_auto_clean: 8 | # Skip Python build cleaning 9 | 10 | override_dh_auto_configure: 11 | # Skip Python configure 12 | 13 | override_dh_auto_build: 14 | # Skip Python build 15 | 16 | override_dh_auto_test: 17 | # Skip Python tests 18 | 19 | override_dh_installdocs: 20 | # Skip docs 21 | 22 | override_dh_installchangelogs: 23 | # Skip changelogs 24 | 25 | override_dh_auto_install: 26 | # Create directories 27 | mkdir -p debian/jottr/usr/bin 28 | mkdir -p debian/jottr/usr/share/applications 29 | mkdir -p debian/jottr/usr/share/jottr 30 | mkdir -p debian/jottr/usr/share/icons/hicolor/128x128/apps 31 | 32 | # Install program files 33 | cp -r src/jottr/* debian/jottr/usr/share/jottr/ 34 | 35 | # Install desktop file 36 | cp packaging/debian/jottr.desktop debian/jottr/usr/share/applications/ 37 | 38 | # Install icon (if it exists) 39 | if [ -f src/jottr/icons/jottr.png ]; then \ 40 | cp src/jottr/icons/jottr.png debian/jottr/usr/share/icons/hicolor/128x128/apps/; \ 41 | fi 42 | 43 | # Create launcher script 44 | echo '#!/bin/sh' > debian/jottr/usr/bin/jottr 45 | echo 'PYTHONPATH=/usr/share/jottr exec python3 /usr/share/jottr/main.py "$$@"' >> debian/jottr/usr/bin/jottr 46 | chmod +x debian/jottr/usr/bin/jottr 47 | -------------------------------------------------------------------------------- /packaging/debian/test-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Colors for output 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | NC='\033[0m' 8 | 9 | echo "Testing Jottr package..." 10 | 11 | # Find the latest built package 12 | DEB_PACKAGE=$(ls -t deb_dist/python3-jottr*.deb | head -n1) 13 | if [ ! -f "$DEB_PACKAGE" ]; then 14 | echo -e "${RED}Error: No .deb package found in deb_dist/${NC}" 15 | exit 1 16 | fi 17 | 18 | # Install the package 19 | echo "Installing package: $DEB_PACKAGE" 20 | sudo dpkg -i "$DEB_PACKAGE" || sudo apt-get install -f -y 21 | 22 | # Check if binary is installed 23 | if ! which jottr > /dev/null; then 24 | echo -e "${RED}Error: jottr binary not found in PATH${NC}" 25 | exit 1 26 | fi 27 | 28 | # Check if desktop file is installed 29 | if [ ! -f "/usr/share/applications/jottr.desktop" ]; then 30 | echo -e "${RED}Error: Desktop file not installed${NC}" 31 | exit 1 32 | fi 33 | 34 | # Check if icon is installed 35 | if [ ! -f "/usr/share/icons/hicolor/128x128/apps/jottr.png" ]; then 36 | echo -e "${RED}Error: Application icon not installed${NC}" 37 | exit 1 38 | fi 39 | 40 | # Try to import the module 41 | if ! python3 -c "import jottr" 2>/dev/null; then 42 | echo -e "${RED}Error: Cannot import jottr module${NC}" 43 | exit 1 44 | fi 45 | 46 | echo -e "${GREEN}All tests passed successfully!${NC}" 47 | 48 | # Optional: Remove the package 49 | read -p "Do you want to remove the test package? [y/N] " -n 1 -r 50 | echo 51 | if [[ $REPLY =~ ^[Yy]$ ]]; then 52 | sudo apt-get remove -y python3-jottr 53 | fi 54 | -------------------------------------------------------------------------------- /packaging/rpm/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$1 4 | PACKAGE_NAME="jottr" 5 | PACKAGE_VERSION="${VERSION:-1.0.0}" 6 | 7 | # Create RPM build structure 8 | mkdir -p {BUILD,RPMS,SOURCES,SPECS,SRPMS} 9 | 10 | # Create source distribution 11 | cd ../.. 12 | python -m build --sdist 13 | cd packaging/rpm 14 | cp ../../dist/${PACKAGE_NAME}-${PACKAGE_VERSION}.tar.gz SOURCES/ 15 | 16 | # Create spec file 17 | cat > SPECS/jottr.spec << EOF 18 | %global __python %{__python3} 19 | 20 | Name: python3-jottr 21 | Version: ${PACKAGE_VERSION} 22 | Release: 1%{?dist} 23 | Summary: Modern text editor for writers 24 | 25 | License: MIT 26 | URL: https://github.com/yourusername/jottr 27 | Source0: %{name}-%{version}.tar.gz 28 | BuildArch: noarch 29 | 30 | BuildRequires: python3-devel 31 | BuildRequires: python3-setuptools 32 | Requires: python3-qt5 33 | Requires: python3-qt5-webengine 34 | Requires: python3-enchant 35 | %description 36 | Jottr is a feature-rich text editor designed specifically 37 | for writers and journalists, with features like smart 38 | completion, snippets, and integrated web browsing. 39 | 40 | %prep 41 | %autosetup -n jottr-%{version} 42 | 43 | %build 44 | %py3_build 45 | 46 | %install 47 | %py3_install 48 | 49 | # Install desktop file 50 | mkdir -p %{buildroot}%{_datadir}/applications/ 51 | cat > %{buildroot}%{_datadir}/applications/jottr.desktop << EOL 52 | [Desktop Entry] 53 | Name=Jottr 54 | Comment=Modern text editor for writers 55 | Exec=jottr 56 | Icon=jottr 57 | Terminal=false 58 | Type=Application 59 | Categories=Office;TextEditor; 60 | EOL 61 | 62 | %files 63 | %{python3_sitelib}/jottr/ 64 | %{python3_sitelib}/jottr-%{version}* 65 | %{_bindir}/jottr 66 | %{_datadir}/applications/jottr.desktop 67 | 68 | %changelog 69 | * $(date '+%a %b %d %Y') Package BuilderPublished: {entry.published}
" 198 | if hasattr(entry, 'description'): 199 | content += f"{entry.description}
" 200 | if hasattr(entry, 'link'): 201 | content += f'' 202 | self.content_viewer.setHtml(content) 203 | 204 | def manage_feeds(self): 205 | dialog = FeedManagerDialog(self.feeds, self) 206 | if dialog.exec_() == QDialog.Accepted: 207 | self.feeds = dialog.get_feeds() 208 | self.save_feeds() 209 | self.update_feed_selector() 210 | self.refresh_current_feed() -------------------------------------------------------------------------------- /src/jottr/rss_tab.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QWidget, QVBoxLayout 2 | from rss_reader import RSSReader 3 | 4 | class RSSTab(QWidget): 5 | def __init__(self, parent=None): 6 | super().__init__(parent) 7 | layout = QVBoxLayout(self) 8 | self.rss_reader = RSSReader() 9 | layout.addWidget(self.rss_reader) -------------------------------------------------------------------------------- /src/jottr/settings_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, 2 | QLineEdit, QPushButton, QListWidget, QTabWidget, 3 | QWidget, QCheckBox, QMessageBox, QInputDialog, QComboBox) 4 | from PyQt5.QtCore import Qt 5 | import json 6 | import os 7 | 8 | class SettingsDialog(QDialog): 9 | def __init__(self, settings_manager, parent=None): 10 | super().__init__(parent) 11 | self.settings_manager = settings_manager 12 | self.setWindowTitle("Settings") 13 | self.setMinimumWidth(500) 14 | 15 | self.setup_ui() 16 | 17 | def setup_ui(self): 18 | """Setup the UI components""" 19 | # Create layout 20 | layout = QVBoxLayout(self) 21 | 22 | # Create tab widget 23 | tabs = QTabWidget() 24 | 25 | # Appearance tab 26 | appearance_tab = QWidget() 27 | appearance_layout = QVBoxLayout(appearance_tab) 28 | 29 | # UI Theme 30 | theme_layout = QHBoxLayout() 31 | theme_label = QLabel("UI Theme:") 32 | self.theme_combo = QComboBox() 33 | self.theme_combo.addItems(['system', 'Fusion', 'Windows', 'Macintosh']) 34 | self.theme_combo.setCurrentText(self.settings_manager.get_ui_theme()) 35 | theme_layout.addWidget(theme_label) 36 | theme_layout.addWidget(self.theme_combo) 37 | appearance_layout.addLayout(theme_layout) 38 | 39 | # Add appearance tab 40 | tabs.addTab(appearance_tab, "Appearance") 41 | 42 | # Browser tab 43 | browser_tab = QWidget() 44 | browser_layout = QVBoxLayout(browser_tab) 45 | 46 | # Homepage setting 47 | homepage_layout = QHBoxLayout() 48 | homepage_label = QLabel("Homepage:") 49 | self.homepage_edit = QLineEdit() 50 | self.homepage_edit.setText(self.settings_manager.get_setting('homepage', 'https://www.apnews.com/')) 51 | homepage_layout.addWidget(homepage_label) 52 | homepage_layout.addWidget(self.homepage_edit) 53 | browser_layout.addLayout(homepage_layout) 54 | 55 | # Search sites 56 | search_label = QLabel("Site-specific searches:") 57 | browser_layout.addWidget(search_label) 58 | 59 | self.search_list = QListWidget() 60 | self.load_search_sites() 61 | browser_layout.addWidget(self.search_list) 62 | 63 | # Search site buttons 64 | search_buttons = QHBoxLayout() 65 | add_search = QPushButton("Add") 66 | edit_search = QPushButton("Edit") 67 | delete_search = QPushButton("Delete") 68 | add_search.clicked.connect(self.add_search_site) 69 | edit_search.clicked.connect(self.edit_search_site) 70 | delete_search.clicked.connect(self.delete_search_site) 71 | search_buttons.addWidget(add_search) 72 | search_buttons.addWidget(edit_search) 73 | search_buttons.addWidget(delete_search) 74 | browser_layout.addLayout(search_buttons) 75 | 76 | # Dictionary tab 77 | dict_tab = QWidget() 78 | dict_layout = QVBoxLayout(dict_tab) 79 | 80 | dict_label = QLabel("User Dictionary:") 81 | dict_layout.addWidget(dict_label) 82 | 83 | self.dict_list = QListWidget() 84 | self.load_user_dict() 85 | dict_layout.addWidget(self.dict_list) 86 | 87 | # Dictionary buttons 88 | dict_buttons = QHBoxLayout() 89 | add_word = QPushButton("Add Word") 90 | delete_word = QPushButton("Delete Word") 91 | add_word.clicked.connect(self.add_dict_word) 92 | delete_word.clicked.connect(self.delete_dict_word) 93 | dict_buttons.addWidget(add_word) 94 | dict_buttons.addWidget(delete_word) 95 | dict_layout.addLayout(dict_buttons) 96 | 97 | # Add tabs 98 | tabs.addTab(browser_tab, "Browser") 99 | tabs.addTab(dict_tab, "Dictionary") 100 | 101 | layout.addWidget(tabs) 102 | 103 | # Dialog buttons 104 | buttons = QHBoxLayout() 105 | ok_button = QPushButton("OK") 106 | cancel_button = QPushButton("Cancel") 107 | ok_button.clicked.connect(self.accept) 108 | cancel_button.clicked.connect(self.reject) 109 | buttons.addWidget(ok_button) 110 | buttons.addWidget(cancel_button) 111 | layout.addLayout(buttons) 112 | 113 | def load_search_sites(self): 114 | """Load search sites from settings""" 115 | sites = self.settings_manager.get_setting('search_sites', { 116 | 'AP News': 'site:apnews.com', 117 | 'Reuters': 'site:reuters.com', 118 | 'BBC News': 'site:bbc.com/news' 119 | }) 120 | for name, site in sites.items(): 121 | self.search_list.addItem(f"{name}: {site}") 122 | 123 | def load_user_dict(self): 124 | """Load user dictionary words""" 125 | words = self.settings_manager.get_setting('user_dictionary', []) 126 | self.dict_list.addItems(words) 127 | 128 | def add_search_site(self): 129 | """Add new search site""" 130 | dialog = SearchSiteDialog(self) 131 | if dialog.exec_(): 132 | name, site = dialog.get_data() 133 | self.search_list.addItem(f"{name}: {site}") 134 | 135 | def edit_search_site(self): 136 | """Edit selected search site""" 137 | current = self.search_list.currentItem() 138 | if current: 139 | name, site = current.text().split(': ', 1) 140 | dialog = SearchSiteDialog(self, name, site) 141 | if dialog.exec_(): 142 | new_name, new_site = dialog.get_data() 143 | current.setText(f"{new_name}: {new_site}") 144 | 145 | def delete_search_site(self): 146 | """Delete selected search site""" 147 | current = self.search_list.currentRow() 148 | if current >= 0: 149 | self.search_list.takeItem(current) 150 | 151 | def add_dict_word(self): 152 | """Add word to user dictionary""" 153 | word, ok = QInputDialog.getText(self, "Add Word", "Enter word:") 154 | if ok and word: 155 | self.dict_list.addItem(word) 156 | 157 | def delete_dict_word(self): 158 | """Delete word from user dictionary""" 159 | current = self.dict_list.currentRow() 160 | if current >= 0: 161 | self.dict_list.takeItem(current) 162 | 163 | def get_data(self): 164 | """Get dialog data""" 165 | return { 166 | 'homepage': self.homepage_edit.text(), 167 | 'search_sites': self.get_search_sites(), 168 | 'user_dictionary': self.get_user_dictionary(), 169 | 'ui_theme': self.theme_combo.currentText() 170 | } 171 | 172 | def get_search_sites(self): 173 | """Get search sites from list widget""" 174 | sites = {} 175 | for i in range(self.search_list.count()): 176 | name, site = self.search_list.item(i).text().split(': ', 1) 177 | sites[name] = site 178 | return sites 179 | 180 | def get_user_dictionary(self): 181 | """Get words from dictionary list widget""" 182 | words = [] 183 | for i in range(self.dict_list.count()): 184 | words.append(self.dict_list.item(i).text()) 185 | return words 186 | 187 | class SearchSiteDialog(QDialog): 188 | def __init__(self, parent=None, name='', site=''): 189 | super().__init__(parent) 190 | self.setWindowTitle("Search Site") 191 | 192 | # Remove 'site:' prefix if it exists for display 193 | if site.startswith('site:'): 194 | site = site[5:] 195 | 196 | layout = QVBoxLayout(self) 197 | 198 | # Name field 199 | name_layout = QHBoxLayout() 200 | name_label = QLabel("Name:") 201 | self.name_edit = QLineEdit(name) 202 | name_layout.addWidget(name_label) 203 | name_layout.addWidget(self.name_edit) 204 | layout.addLayout(name_layout) 205 | 206 | # Site field 207 | site_layout = QHBoxLayout() 208 | site_label = QLabel("Website:") 209 | self.site_edit = QLineEdit(site) 210 | self.site_edit.setPlaceholderText("example.com") 211 | site_layout.addWidget(site_label) 212 | site_layout.addWidget(self.site_edit) 213 | layout.addLayout(site_layout) 214 | 215 | # Add help text 216 | help_label = QLabel("Enter the website domain without 'http://' or 'www.'") 217 | help_label.setStyleSheet("color: gray; font-size: 10px;") 218 | layout.addWidget(help_label) 219 | 220 | # Buttons 221 | buttons = QHBoxLayout() 222 | ok_button = QPushButton("OK") 223 | cancel_button = QPushButton("Cancel") 224 | ok_button.clicked.connect(self.accept) 225 | cancel_button.clicked.connect(self.reject) 226 | buttons.addWidget(ok_button) 227 | buttons.addWidget(cancel_button) 228 | layout.addLayout(buttons) 229 | 230 | def get_data(self): 231 | """Get dialog data with 'site:' prefix automatically added""" 232 | name = self.name_edit.text() 233 | site = self.site_edit.text().strip() 234 | 235 | # Remove any existing 'site:' prefix 236 | if site.startswith('site:'): 237 | site = site[5:] 238 | 239 | # Remove http://, https://, and www. if present 240 | site = site.replace('http://', '').replace('https://', '').replace('www.', '') 241 | 242 | # Add 'site:' prefix 243 | site = f'site:{site}' 244 | 245 | return name, site -------------------------------------------------------------------------------- /src/jottr/settings_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from PyQt5.QtGui import QFont 4 | import time 5 | from PyQt5.QtWidgets import QApplication, QStyleFactory 6 | import sys 7 | 8 | class SettingsManager: 9 | def __init__(self): 10 | # Initialize default settings 11 | self.settings = { 12 | "font_family": "DejaVu Sans Mono", 13 | "font_size": 12, 14 | "font_weight": 50, 15 | "font_italic": False, 16 | "theme": "default", 17 | "ui_theme": "light", 18 | "spell_check": True, 19 | "search_sites": { 20 | "AP News": "site:apnews.com", 21 | "Reuters": "site:reuters.com", 22 | "BBC News": "site:bbc.com/news" 23 | }, 24 | "show_snippets": False, 25 | "show_browser": False, 26 | "pane_states": { 27 | "snippets_visible": False, 28 | "browser_visible": False, 29 | "sizes": [700, 300, 300] 30 | } 31 | } 32 | 33 | # Set up config directory based on platform 34 | if sys.platform == 'darwin': 35 | self.config_dir = os.path.join(os.path.expanduser('~/Library/Application Support'), 'Jottr') 36 | elif sys.platform == 'win32': 37 | self.config_dir = os.path.join(os.getenv('APPDATA'), 'Jottr') 38 | else: # Linux/Unix 39 | CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) 40 | self.config_dir = os.path.join(CONFIG_HOME, "Jottr") 41 | 42 | # Create config directory if it doesn't exist 43 | os.makedirs(self.config_dir, exist_ok=True) 44 | 45 | # Define paths for different data types 46 | self.settings_file = os.path.join(self.config_dir, 'settings.json') 47 | self.snippets_dir = os.path.join(self.config_dir, 'snippets') 48 | self.dict_file = os.path.join(self.config_dir, 'user_dictionary.txt') 49 | 50 | # Create snippets directory 51 | os.makedirs(self.snippets_dir, exist_ok=True) 52 | 53 | # Initialize settings 54 | self.load_settings() 55 | 56 | # # Create autosave directory 57 | # self.autosave_dir = os.path.join(os.path.expanduser("~"), ".ap_editor_autosave") 58 | # os.makedirs(self.autosave_dir, exist_ok=True) 59 | 60 | # # Create running flag file 61 | # self.running_flag = os.path.join(self.autosave_dir, "editor_running") 62 | # # Set running flag 63 | # with open(self.running_flag, 'w') as f: 64 | # f.write(str(os.getpid())) 65 | 66 | # # Setup backup and recovery directories 67 | # self.backup_dir = os.path.join(os.path.expanduser("~"), ".ap_editor", "backups") 68 | # self.recovery_dir = os.path.join(os.path.expanduser("~"), ".ap_editor", "recovery") 69 | # os.makedirs(self.backup_dir, exist_ok=True) 70 | # os.makedirs(self.recovery_dir, exist_ok=True) 71 | 72 | # # Create session file 73 | # self.session_file = os.path.join(self.recovery_dir, "session.json") 74 | # self.create_session_file() 75 | 76 | # # Create session state file 77 | # self.session_state_file = os.path.join(self.recovery_dir, "session_state.json") 78 | 79 | # # Initialize with unclean state 80 | # self.initialize_session_state() 81 | 82 | # Clean up old session files if last exit was clean 83 | # if os.path.exists(self.session_state_file): 84 | # try: 85 | # with open(self.session_state_file, 'r') as f: 86 | # state = json.load(f) 87 | # if state.get('clean_exit', False): 88 | # self.cleanup_old_sessions() 89 | # except: 90 | # pass 91 | 92 | def load_settings(self): 93 | """Load settings from file""" 94 | settings_path = os.path.join(self.config_dir, 'settings.json') 95 | if os.path.exists(settings_path): 96 | try: 97 | with open(settings_path, 'r', encoding='utf-8') as f: 98 | saved_settings = json.load(f) 99 | self.settings.update(saved_settings) 100 | except Exception as e: 101 | print(f"Error loading settings: {str(e)}") 102 | 103 | def save_settings(self): 104 | with open(self.settings_file, 'w') as f: 105 | json.dump(self.settings, f) 106 | 107 | def get_font(self): 108 | font = QFont( 109 | self.settings["font_family"], 110 | self.settings["font_size"], 111 | self.settings["font_weight"] 112 | ) 113 | font.setItalic(self.settings["font_italic"]) 114 | return font 115 | 116 | def save_font(self, font): 117 | self.settings.update({ 118 | "font_family": font.family(), 119 | "font_size": font.pointSize(), 120 | "font_weight": font.weight(), 121 | "font_italic": font.italic() 122 | }) 123 | self.save_settings() 124 | 125 | def get_theme(self): 126 | return self.settings["theme"] 127 | 128 | def save_theme(self, theme): 129 | self.settings["theme"] = theme 130 | self.save_settings() 131 | 132 | def get_pane_visibility(self): 133 | return (self.settings["show_snippets"], self.settings["show_browser"]) 134 | 135 | def save_pane_visibility(self, show_snippets, show_browser): 136 | self.settings.update({ 137 | "show_snippets": show_snippets, 138 | "show_browser": show_browser 139 | }) 140 | self.save_settings() 141 | 142 | # def save_last_files(self, files): 143 | # """Save list of last opened files""" 144 | # self.settings["last_files"] = files 145 | # self.save_settings() 146 | 147 | # def get_last_files(self): 148 | # """Get list of last opened files""" 149 | # return self.settings.get("last_files", []) 150 | 151 | # def get_autosave_dir(self): 152 | # """Get the directory for autosave files""" 153 | # return self.autosave_dir 154 | 155 | # def cleanup_autosave_dir(self): 156 | # """Clean up old autosave files""" 157 | # if os.path.exists(self.autosave_dir): 158 | # try: 159 | # # Remove files older than 7 days 160 | # for filename in os.listdir(self.autosave_dir): 161 | # filepath = os.path.join(self.autosave_dir, filename) 162 | # if os.path.getmtime(filepath) < time.time() - 7 * 86400: 163 | # os.remove(filepath) 164 | # except: 165 | # pass 166 | 167 | # def clear_running_flag(self): 168 | # """Clear the running flag on clean exit""" 169 | # try: 170 | # if os.path.exists(self.running_flag): 171 | # os.remove(self.running_flag) 172 | # except: 173 | # pass 174 | 175 | # def was_previous_crash(self): 176 | # """Check if previous session crashed""" 177 | # if os.path.exists(self.running_flag): 178 | # try: 179 | # with open(self.running_flag, 'r') as f: 180 | # old_pid = int(f.read().strip()) 181 | # # Check if the process is still running 182 | # try: 183 | # os.kill(old_pid, 0) 184 | # # If we get here, the process is still running 185 | # return False 186 | # except OSError: 187 | # # Process is not running, was a crash 188 | # return True 189 | # except: 190 | # return True 191 | # return False 192 | 193 | # def create_session_file(self): 194 | # """Create a session file to track clean/dirty exits""" 195 | # session_data = { 196 | # 'pid': os.getpid(), 197 | # 'timestamp': time.time(), 198 | # 'clean_exit': False 199 | # } 200 | # try: 201 | # with open(self.session_file, 'w') as f: 202 | # json.dump(session_data, f) 203 | # except Exception as e: 204 | # print(f"Failed to create session file: {str(e)}") 205 | 206 | # def mark_clean_exit(self): 207 | # """Mark that the editor exited cleanly""" 208 | # try: 209 | # if os.path.exists(self.session_file): 210 | # with open(self.session_file, 'r') as f: 211 | # session_data = json.load(f) 212 | # session_data['clean_exit'] = True 213 | # with open(self.session_file, 'w') as f: 214 | # json.dump(session_data, f) 215 | # except Exception as e: 216 | # print(f"Failed to mark clean exit: {str(e)}") 217 | 218 | # def needs_recovery(self): 219 | # """Check if we need to recover from a crash""" 220 | # try: 221 | # if os.path.exists(self.session_file): 222 | # with open(self.session_file, 'r') as f: 223 | # session_data = json.load(f) 224 | # return not session_data.get('clean_exit', True) 225 | # return True # If no session file, assume we need recovery 226 | # except Exception as e: 227 | # print(f"Error checking recovery status: {str(e)}") 228 | # return True # If we can't read the session file, assume we need recovery 229 | 230 | # def get_backup_dir(self): 231 | # return self.backup_dir 232 | 233 | # def get_recovery_dir(self): 234 | # return self.recovery_dir 235 | 236 | # def initialize_session_state(self): 237 | # """Initialize or update session state""" 238 | # try: 239 | # state = { 240 | # 'clean_exit': False, 241 | # 'timestamp': time.time(), 242 | # 'open_tabs': [] 243 | # } 244 | # with open(self.session_state_file, 'w') as f: 245 | # json.dump(state, f) 246 | # except Exception as e: 247 | # print(f"Failed to initialize session state: {str(e)}") 248 | 249 | # def save_session_state(self, tab_ids, clean_exit=False): 250 | # """Save the list of currently open tab IDs""" 251 | # try: 252 | # state = { 253 | # 'open_tabs': tab_ids, 254 | # 'clean_exit': clean_exit, 255 | # 'timestamp': time.time() 256 | # } 257 | # with open(self.session_state_file, 'w') as f: 258 | # json.dump(state, f) 259 | # except Exception as e: 260 | # print(f"Failed to save session state: {str(e)}") 261 | 262 | # def get_session_state(self): 263 | # """Get list of tab IDs that were open in last session""" 264 | # try: 265 | # if os.path.exists(self.session_state_file): 266 | # with open(self.session_state_file, 'r') as f: 267 | # state = json.load(f) 268 | # return state.get('open_tabs', []) 269 | # except Exception as e: 270 | # print(f"Failed to load session state: {str(e)}") 271 | # return [] 272 | 273 | # def cleanup_old_sessions(self): 274 | # """Clean up session files from previous clean exits""" 275 | # # Don't clean up by default - let the session restore handle it 276 | # pass 277 | 278 | def get_setting(self, key, default=None): 279 | """Get a setting value with a default fallback""" 280 | try: 281 | return self.settings.get(key, default) 282 | except Exception as e: 283 | print(f"Failed to get setting {key}: {str(e)}") 284 | return default 285 | 286 | def save_setting(self, key, value): 287 | """Save a single setting""" 288 | try: 289 | self.settings[key] = value 290 | with open(self.settings_file, 'w') as f: 291 | json.dump(self.settings, f) 292 | except Exception as e: 293 | print(f"Failed to save setting {key}: {str(e)}") 294 | 295 | def get_ui_theme(self): 296 | """Get current UI theme setting""" 297 | return self.settings.get('ui_theme', 'system') 298 | 299 | def save_ui_theme(self, theme): 300 | """Save UI theme setting""" 301 | self.settings['ui_theme'] = theme 302 | self.save_settings() 303 | 304 | def apply_ui_theme(self, theme): 305 | """Apply UI theme to application""" 306 | if theme == 'system': 307 | # Use system default theme 308 | QApplication.setStyle(QStyleFactory.create('Fusion')) 309 | else: 310 | # Apply custom theme 311 | QApplication.setStyle(QStyleFactory.create(theme)) 312 | 313 | # Save the theme 314 | self.save_ui_theme(theme) -------------------------------------------------------------------------------- /src/jottr/snippet_editor_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, 2 | QTextEdit, QPushButton, QLabel) 3 | 4 | class SnippetEditorDialog(QDialog): 5 | def __init__(self, title="", content="", parent=None): 6 | super().__init__(parent) 7 | self.setWindowTitle("Edit Snippet") 8 | self.setMinimumWidth(500) 9 | self.setMinimumHeight(400) 10 | 11 | layout = QVBoxLayout(self) 12 | 13 | # Title input 14 | title_layout = QHBoxLayout() 15 | title_label = QLabel("Title:") 16 | self.title_edit = QLineEdit(title) 17 | title_layout.addWidget(title_label) 18 | title_layout.addWidget(self.title_edit) 19 | layout.addLayout(title_layout) 20 | 21 | # Content input 22 | content_label = QLabel("Content:") 23 | layout.addWidget(content_label) 24 | self.content_edit = QTextEdit() 25 | self.content_edit.setPlainText(content) 26 | layout.addWidget(self.content_edit) 27 | 28 | # Buttons 29 | button_layout = QHBoxLayout() 30 | save_button = QPushButton("Save") 31 | cancel_button = QPushButton("Cancel") 32 | button_layout.addWidget(save_button) 33 | button_layout.addWidget(cancel_button) 34 | layout.addLayout(button_layout) 35 | 36 | save_button.clicked.connect(self.accept) 37 | cancel_button.clicked.connect(self.reject) 38 | 39 | def get_data(self): 40 | return { 41 | 'title': self.title_edit.text(), 42 | 'content': self.content_edit.toPlainText() 43 | } -------------------------------------------------------------------------------- /src/jottr/snippet_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | class SnippetManager: 5 | def __init__(self, settings_manager): 6 | self.settings_manager = settings_manager 7 | self.file_path = os.path.join( 8 | self.settings_manager.config_dir, 9 | 'snippets.json' 10 | ) 11 | self.snippets = {} 12 | self.load_snippets() 13 | 14 | def load_snippets(self): 15 | """Load snippets from file""" 16 | if os.path.exists(self.file_path): 17 | try: 18 | with open(self.file_path, 'r', encoding='utf-8') as f: 19 | self.snippets = json.load(f) 20 | except Exception as e: 21 | print(f"Error loading snippets: {str(e)}") 22 | self.snippets = {} 23 | 24 | def save_snippets(self): 25 | with open(self.file_path, 'w') as file: 26 | json.dump(self.snippets, file) 27 | 28 | def add_snippet(self, title, text): 29 | self.snippets[title] = text 30 | self.save_snippets() 31 | 32 | def get_snippet(self, title): 33 | return self.snippets.get(title) 34 | 35 | def get_snippets(self): 36 | return list(self.snippets.keys()) 37 | 38 | def delete_snippet(self, title): 39 | if title in self.snippets: 40 | del self.snippets[title] 41 | self.save_snippets() 42 | 43 | def get_all_snippet_contents(self): 44 | """Return a list of all snippet contents""" 45 | return list(self.snippets.values()) -------------------------------------------------------------------------------- /src/jottr/theme_manager.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtGui import QColor, QPalette 2 | from PyQt5.QtWidgets import QStyleFactory 3 | 4 | class ThemeManager: 5 | @staticmethod 6 | def get_themes(): 7 | return { 8 | "Light": { 9 | "bg": "#ffffff", 10 | "text": "#000000", 11 | "selection": "#b3d4fc" 12 | }, 13 | "Dark": { 14 | "bg": "#1e1e1e", 15 | "text": "#d4d4d4", 16 | "selection": "#264f78" 17 | }, 18 | "Sepia": { 19 | "bg": "#f4ecd8", 20 | "text": "#5b4636", 21 | "selection": "#c4b5a0" 22 | } 23 | } 24 | 25 | @staticmethod 26 | def apply_theme(editor, theme_name): 27 | themes = ThemeManager.get_themes() 28 | if theme_name in themes: 29 | theme = themes[theme_name] 30 | editor.setStyleSheet(f""" 31 | QTextEdit {{ 32 | background-color: {theme['bg']}; 33 | color: {theme['text']}; 34 | selection-background-color: {theme['selection']}; 35 | font-family: {editor.font().family()}; 36 | font-size: {editor.font().pointSize()}pt; 37 | }} 38 | """) --------------------------------------------------------------------------------