diff --git a/main.py b/main.py index 7026625..aa6123b 100644 --- a/main.py +++ b/main.py @@ -15,9 +15,10 @@ Version: 0.1 import pathlib import platform import sys +import threading import webbrowser import yaml -from PyQt6.QtCore import QFileSystemWatcher, Qt +from PyQt6.QtCore import QFileSystemWatcher, Qt, QTimer from PyQt6.QtGui import QAction, QStandardItem, QStandardItemModel, QIcon, QPalette from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineCore import QWebEngineSettings @@ -25,6 +26,9 @@ from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QHBoxLayout, Q QMessageBox, QTreeView, QFileDialog, QAbstractItemView, QLineEdit, QVBoxLayout from markdown2 import markdown from fuzzywuzzy import process, fuzz +import logging + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') class MarkdownEditor(QMainWindow): @@ -43,6 +47,7 @@ class MarkdownEditor(QMainWindow): def __init__(self): """ Initializes the Markdown editor application with UI setup and configuration management. """ + logging.info("Initializing the Markdown Editor...") super().__init__() self.dataDirectory = None self.preview = None @@ -62,100 +67,115 @@ class MarkdownEditor(QMainWindow): self.update_directory_tree_view() self.fileSystemWatcher.directoryChanged.connect(self.update_directory_tree_view) self.load_initial_file() + self.searchTimer = QTimer() + self.searchTimer.setSingleShot(True) + self.searchTimer.timeout.connect(self.perform_search) def initialize_ui(self): """ Sets up the application's user interface components. """ + logging.info("Setting up the user interface...") self.setWindowTitle("Markdown Editor") width = self.config['window']['width'] height = self.config['window']['height'] self.setGeometry(100, 100, width, height) icon_path = self.dataDirectory / 'data' / 'smile.png' self.setWindowIcon(QIcon(str(icon_path))) - - # Create the directory tree view self.directoryTree = QTreeView() self.directoryTree.setHeaderHidden(True) self.directoryTree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.directoryTree.doubleClicked.connect(self.on_file_double_clicked) - - # Create the QLineEdit widget for the search box self.searchBox = QLineEdit() - self.searchBox.setPlaceholderText("Search...") # Placeholder text for the search box - self.searchBox.textChanged.connect(self.on_search_text_changed) # Connect to search handler - - # Create a QVBoxLayout for the directory tree and search box + self.searchBox.setPlaceholderText("Search...") + self.searchBox.textChanged.connect(self.on_search_text_changed) directory_layout = QVBoxLayout() - directory_layout.addWidget(self.searchBox) # Add the search box to the layout - directory_layout.addWidget(self.directoryTree) # Add the directory tree below the search box - - # Text editor setup + directory_layout.addWidget(self.searchBox) + directory_layout.addWidget(self.directoryTree) self.editor = QTextEdit() self.editor.textChanged.connect(self.update_preview) - - # Preview setup self.preview = QWebEngineView() - web_engine_settings = self.preview.page().settings() web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True) web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True) - - # Main layout setup layout = QHBoxLayout() - layout.addLayout(directory_layout, 0) # Add the QVBoxLayout with the search box and directory tree + layout.addLayout(directory_layout, 0) layout.addWidget(self.editor, 1) layout.addWidget(self.preview, 1) - - # Set the main layout container = QWidget() container.setLayout(layout) self.setCentralWidget(container) - - # Toolbar, menu bar, and status bar setup self.create_tool_bar() self.create_menu_bar() self.setStatusBar(QStatusBar(self)) - def initialize_data_directory(self): - """ Initializes the data directory and sets up the file system watcher. """ - data_directory = self.config['dataDirectory'] - self.dataDirectory = pathlib.Path(data_directory).resolve() - if not self.dataDirectory.is_dir(): - QMessageBox.critical(self, "Directory Error", "The application's directory is not valid.") + """ + Initializes the data directory and sets up the file system watcher. + """ + logging.info("Initializing data directory.") + try: + data_directory = self.config.get('dataDirectory', str(pathlib.Path.home())) + self.dataDirectory = pathlib.Path(data_directory).resolve() + if not self.dataDirectory.is_dir(): + raise FileNotFoundError(f"Data directory not found: {self.dataDirectory}") + self.fileSystemWatcher.addPath(str(self.dataDirectory)) + logging.info(f"Data directory set to: {self.dataDirectory}") + except Exception as e: + logging.error(f"Failed to initialize data directory: {e}") + QMessageBox.critical(self, "Directory Error", str(e)) sys.exit(1) - self.fileSystemWatcher.addPath(str(self.dataDirectory)) - def on_search_text_changed(self, text): - """ - Handles the search functionality. - First, tries an exact match. If no results are found, uses fuzzy matching and gradually increases fuzziness. - """ - if not self.dataDirectory or not self.dataDirectory.is_dir(): - return + self.searchText = text + if not text: + self.update_directory_tree_view() + else: + self.searchTimer.start(60) + + def update_search_results(self, text): model = QStandardItemModel() + self.populate_filtered_model(model, self.dataDirectory, text) self.directoryTree.setModel(model) - try: - entries = list(self.dataDirectory.rglob('*')) - filtered_entries = [] - if text: - filtered_entries = [entry for entry in entries if text.lower() in entry.name.lower()] - fuzziness = 100 - while not filtered_entries and fuzziness > 0: - matches = [(entry, fuzz.partial_ratio(text.lower(), entry.name.lower())) - for entry in entries] - filtered_entries = [entry for entry, score in matches if score >= fuzziness] - if len(filtered_entries) >= 2: - break - fuzziness -= 5 - for entry in filtered_entries: - self.add_item_to_model(model, entry, is_dir=entry.is_dir()) - except Exception as e: - QMessageBox.critical(self, "Search Error", f"An error occurred during the search: {e}") + def perform_search(self): + threading.Thread(target=self.update_search_results, args=(self.searchText,)).start() + def populate_filtered_model(self, parent_item, directory, text, fuzziness=100, depth=0): + """ + Recursively populates the directory tree model with filtered contents based on the search text. - def add_item_to_model(self, model, entry, is_dir=False): + Args: + parent_item (QStandardItem): The parent item in the tree view model. + directory (pathlib.Path): The directory to scan. + text (str): The search text. + fuzziness (int): The fuzziness level for matching file names. + depth (int): Current depth of recursion. + """ + if depth > self.depth: + return False + dir_added = False + for entry in sorted(directory.iterdir(), key=lambda e: (e.is_file(), e.name.lower())): + if entry.is_dir(): + dir_item = QStandardItem(entry.name) + if self.populate_filtered_model(dir_item, entry, text, fuzziness, depth + 1): + parent_item.appendRow(dir_item) + dir_added = True + elif entry.suffix == '.md' and self.is_match(entry.name, text, fuzziness): + file_item = QStandardItem(entry.name) + file_item.setData(str(entry)) + parent_item.appendRow(file_item) + dir_added = True + return dir_added + + @staticmethod + def is_match(filename, text, fuzziness): + if not text: + return False + if fuzziness == 100: + return text.lower() in filename.lower() + return fuzz.partial_ratio(text.lower(), filename.lower()) >= fuzziness + + @staticmethod + def add_item_to_model(model, entry, is_dir=False): """ Adds an item to the directory tree model. @@ -171,7 +191,6 @@ class MarkdownEditor(QMainWindow): item.setIcon(QIcon('icons/folder_icon.png')) model.appendRow(item) - @staticmethod def get_config_path(): """ @@ -211,10 +230,22 @@ class MarkdownEditor(QMainWindow): Load the configuration from the configuration file. Returns: - dict: The loaded configuration. + dict: The loaded configuration with default values if keys are missing. """ - with open(self.config_path) as file: - return yaml.safe_load(file) + default_config = { + 'window': {'width': 800, 'height': 600}, + 'dataDirectory': str(pathlib.Path(__file__).parent.resolve()), + 'depth': 5 + } + try: + with open(self.config_path) as file: + config = yaml.safe_load(file) or {} + except FileNotFoundError: + config = {} + for key, value in default_config.items(): + config.setdefault(key, value) + self.depth = config['depth'] + return config def save_config(self): """ @@ -495,30 +526,53 @@ class MarkdownEditor(QMainWindow): def populate_model(self, parent_item, directory, depth=0): """ Populates the directory tree model with directory contents. + Shows all directories containing Markdown files and Markdown files themselves. Args: parent_item (QStandardItem): Item to populate. directory (pathlib.Path): Directory to scan. depth (int): Depth level for recursive scanning. """ - if depth > 5: + if depth > self.depth: return + try: for entry in sorted(directory.iterdir(), key=lambda e: (e.is_file(), e.name.lower())): if entry.is_dir(): - if any(file.suffix == '.md' for file in entry.glob('*.md')): - dir_item = QStandardItem(entry.name) - dir_item.setData(str(entry)) - dir_item.setFlags(dir_item.flags() | Qt.ItemFlag.ItemIsEnabled) - dir_item.setIcon(QIcon('icons/folder_icon.png')) - parent_item.appendRow(dir_item) - self.populate_model(dir_item, entry, depth + 1) + self.process_directory(entry, parent_item, depth) elif entry.suffix == '.md': - file_item = QStandardItem(entry.name) - file_item.setData(str(entry)) - parent_item.appendRow(file_item) - except PermissionError: - pass + self.add_file_item(entry, parent_item) + except PermissionError as e: + logging.error(f"Permission error accessing {directory}: {e}") + + @staticmethod + def add_file_item(file_path, parent_item): + """ + Adds a file item to the model. + + Args: + file_path (pathlib.Path): The file path to add. + parent_item (QStandardItem): The parent item in the model. + """ + file_item = QStandardItem(file_path.name) + file_item.setData(str(file_path)) + parent_item.appendRow(file_item) + + def process_directory(self, directory, parent_item, depth): + """ + Processes a single directory, adding it to the model if it contains Markdown files. + + Args: + directory (pathlib.Path): The directory to process. + parent_item (QStandardItem): The parent item in the model. + depth (int): Current depth in the directory tree. + """ + if any(file.suffix == '.md' for file in directory.iterdir()): + dir_item = QStandardItem(directory.name) + dir_item.setData(str(directory)) + dir_item.setIcon(QIcon('icons/folder_icon.png')) + parent_item.appendRow(dir_item) + self.populate_model(dir_item, directory, depth + 1) def on_directory_tree_clicked(self, index): """ @@ -655,6 +709,7 @@ class MarkdownEditor(QMainWindow): def main(): """ Main function to run the Markdown Editor application. """ + logging.info("Launching the application...") app = QApplication(sys.argv) editor = MarkdownEditor() editor.show()