fuzzy search implemented

This commit is contained in:
Isaak Buslovich 2024-01-16 21:55:49 +01:00
parent e8fdb820ca
commit d6d09ba405
Signed by: Isaak
GPG Key ID: EEC31D6437FBCC63

191
main.py
View File

@ -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']
"""
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():
QMessageBox.critical(self, "Directory Error", "The application's directory is not valid.")
sys.exit(1)
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)
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.
"""
default_config = {
'window': {'width': 800, 'height': 600},
'dataDirectory': str(pathlib.Path(__file__).parent.resolve()),
'depth': 5
}
try:
with open(self.config_path) as file:
return yaml.safe_load(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)
self.process_directory(entry, parent_item, depth)
elif entry.suffix == '.md':
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, entry, depth + 1)
elif entry.suffix == '.md':
file_item = QStandardItem(entry.name)
file_item.setData(str(entry))
parent_item.appendRow(file_item)
except PermissionError:
pass
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()