fuzzy search implemented
This commit is contained in:
parent
e8fdb820ca
commit
d6d09ba405
191
main.py
191
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']
|
||||
"""
|
||||
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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user