fuzzy search implemented
This commit is contained in:
parent
e8fdb820ca
commit
d6d09ba405
199
main.py
199
main.py
@ -15,9 +15,10 @@ Version: 0.1
|
|||||||
import pathlib
|
import pathlib
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import yaml
|
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.QtGui import QAction, QStandardItem, QStandardItemModel, QIcon, QPalette
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtWebEngineCore import QWebEngineSettings
|
from PyQt6.QtWebEngineCore import QWebEngineSettings
|
||||||
@ -25,6 +26,9 @@ from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QHBoxLayout, Q
|
|||||||
QMessageBox, QTreeView, QFileDialog, QAbstractItemView, QLineEdit, QVBoxLayout
|
QMessageBox, QTreeView, QFileDialog, QAbstractItemView, QLineEdit, QVBoxLayout
|
||||||
from markdown2 import markdown
|
from markdown2 import markdown
|
||||||
from fuzzywuzzy import process, fuzz
|
from fuzzywuzzy import process, fuzz
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
|
||||||
class MarkdownEditor(QMainWindow):
|
class MarkdownEditor(QMainWindow):
|
||||||
@ -43,6 +47,7 @@ class MarkdownEditor(QMainWindow):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
""" Initializes the Markdown editor application with UI setup and configuration management. """
|
""" Initializes the Markdown editor application with UI setup and configuration management. """
|
||||||
|
logging.info("Initializing the Markdown Editor...")
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.dataDirectory = None
|
self.dataDirectory = None
|
||||||
self.preview = None
|
self.preview = None
|
||||||
@ -62,100 +67,115 @@ class MarkdownEditor(QMainWindow):
|
|||||||
self.update_directory_tree_view()
|
self.update_directory_tree_view()
|
||||||
self.fileSystemWatcher.directoryChanged.connect(self.update_directory_tree_view)
|
self.fileSystemWatcher.directoryChanged.connect(self.update_directory_tree_view)
|
||||||
self.load_initial_file()
|
self.load_initial_file()
|
||||||
|
self.searchTimer = QTimer()
|
||||||
|
self.searchTimer.setSingleShot(True)
|
||||||
|
self.searchTimer.timeout.connect(self.perform_search)
|
||||||
|
|
||||||
def initialize_ui(self):
|
def initialize_ui(self):
|
||||||
""" Sets up the application's user interface components. """
|
""" Sets up the application's user interface components. """
|
||||||
|
logging.info("Setting up the user interface...")
|
||||||
self.setWindowTitle("Markdown Editor")
|
self.setWindowTitle("Markdown Editor")
|
||||||
width = self.config['window']['width']
|
width = self.config['window']['width']
|
||||||
height = self.config['window']['height']
|
height = self.config['window']['height']
|
||||||
self.setGeometry(100, 100, width, height)
|
self.setGeometry(100, 100, width, height)
|
||||||
icon_path = self.dataDirectory / 'data' / 'smile.png'
|
icon_path = self.dataDirectory / 'data' / 'smile.png'
|
||||||
self.setWindowIcon(QIcon(str(icon_path)))
|
self.setWindowIcon(QIcon(str(icon_path)))
|
||||||
|
|
||||||
# Create the directory tree view
|
|
||||||
self.directoryTree = QTreeView()
|
self.directoryTree = QTreeView()
|
||||||
self.directoryTree.setHeaderHidden(True)
|
self.directoryTree.setHeaderHidden(True)
|
||||||
self.directoryTree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
self.directoryTree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
self.directoryTree.doubleClicked.connect(self.on_file_double_clicked)
|
self.directoryTree.doubleClicked.connect(self.on_file_double_clicked)
|
||||||
|
|
||||||
# Create the QLineEdit widget for the search box
|
|
||||||
self.searchBox = QLineEdit()
|
self.searchBox = QLineEdit()
|
||||||
self.searchBox.setPlaceholderText("Search...") # Placeholder text for the search box
|
self.searchBox.setPlaceholderText("Search...")
|
||||||
self.searchBox.textChanged.connect(self.on_search_text_changed) # Connect to search handler
|
self.searchBox.textChanged.connect(self.on_search_text_changed)
|
||||||
|
|
||||||
# Create a QVBoxLayout for the directory tree and search box
|
|
||||||
directory_layout = QVBoxLayout()
|
directory_layout = QVBoxLayout()
|
||||||
directory_layout.addWidget(self.searchBox) # Add the search box to the layout
|
directory_layout.addWidget(self.searchBox)
|
||||||
directory_layout.addWidget(self.directoryTree) # Add the directory tree below the search box
|
directory_layout.addWidget(self.directoryTree)
|
||||||
|
|
||||||
# Text editor setup
|
|
||||||
self.editor = QTextEdit()
|
self.editor = QTextEdit()
|
||||||
self.editor.textChanged.connect(self.update_preview)
|
self.editor.textChanged.connect(self.update_preview)
|
||||||
|
|
||||||
# Preview setup
|
|
||||||
self.preview = QWebEngineView()
|
self.preview = QWebEngineView()
|
||||||
|
|
||||||
web_engine_settings = self.preview.page().settings()
|
web_engine_settings = self.preview.page().settings()
|
||||||
web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
|
web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
|
||||||
web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
|
web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
|
||||||
|
|
||||||
# Main layout setup
|
|
||||||
layout = QHBoxLayout()
|
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.editor, 1)
|
||||||
layout.addWidget(self.preview, 1)
|
layout.addWidget(self.preview, 1)
|
||||||
|
|
||||||
# Set the main layout
|
|
||||||
container = QWidget()
|
container = QWidget()
|
||||||
container.setLayout(layout)
|
container.setLayout(layout)
|
||||||
self.setCentralWidget(container)
|
self.setCentralWidget(container)
|
||||||
|
|
||||||
# Toolbar, menu bar, and status bar setup
|
|
||||||
self.create_tool_bar()
|
self.create_tool_bar()
|
||||||
self.create_menu_bar()
|
self.create_menu_bar()
|
||||||
self.setStatusBar(QStatusBar(self))
|
self.setStatusBar(QStatusBar(self))
|
||||||
|
|
||||||
|
|
||||||
def initialize_data_directory(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.
|
||||||
self.dataDirectory = pathlib.Path(data_directory).resolve()
|
"""
|
||||||
if not self.dataDirectory.is_dir():
|
logging.info("Initializing data directory.")
|
||||||
QMessageBox.critical(self, "Directory Error", "The application's directory is not valid.")
|
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)
|
sys.exit(1)
|
||||||
self.fileSystemWatcher.addPath(str(self.dataDirectory))
|
|
||||||
|
|
||||||
|
|
||||||
def on_search_text_changed(self, text):
|
def on_search_text_changed(self, text):
|
||||||
"""
|
self.searchText = text
|
||||||
Handles the search functionality.
|
if not text:
|
||||||
First, tries an exact match. If no results are found, uses fuzzy matching and gradually increases fuzziness.
|
self.update_directory_tree_view()
|
||||||
"""
|
else:
|
||||||
if not self.dataDirectory or not self.dataDirectory.is_dir():
|
self.searchTimer.start(60)
|
||||||
return
|
|
||||||
|
def update_search_results(self, text):
|
||||||
model = QStandardItemModel()
|
model = QStandardItemModel()
|
||||||
|
self.populate_filtered_model(model, self.dataDirectory, text)
|
||||||
self.directoryTree.setModel(model)
|
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.
|
Adds an item to the directory tree model.
|
||||||
|
|
||||||
@ -171,7 +191,6 @@ class MarkdownEditor(QMainWindow):
|
|||||||
item.setIcon(QIcon('icons/folder_icon.png'))
|
item.setIcon(QIcon('icons/folder_icon.png'))
|
||||||
model.appendRow(item)
|
model.appendRow(item)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_config_path():
|
def get_config_path():
|
||||||
"""
|
"""
|
||||||
@ -211,10 +230,22 @@ class MarkdownEditor(QMainWindow):
|
|||||||
Load the configuration from the configuration file.
|
Load the configuration from the configuration file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: The loaded configuration.
|
dict: The loaded configuration with default values if keys are missing.
|
||||||
"""
|
"""
|
||||||
with open(self.config_path) as file:
|
default_config = {
|
||||||
return yaml.safe_load(file)
|
'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):
|
def save_config(self):
|
||||||
"""
|
"""
|
||||||
@ -495,30 +526,53 @@ class MarkdownEditor(QMainWindow):
|
|||||||
def populate_model(self, parent_item, directory, depth=0):
|
def populate_model(self, parent_item, directory, depth=0):
|
||||||
"""
|
"""
|
||||||
Populates the directory tree model with directory contents.
|
Populates the directory tree model with directory contents.
|
||||||
|
Shows all directories containing Markdown files and Markdown files themselves.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
parent_item (QStandardItem): Item to populate.
|
parent_item (QStandardItem): Item to populate.
|
||||||
directory (pathlib.Path): Directory to scan.
|
directory (pathlib.Path): Directory to scan.
|
||||||
depth (int): Depth level for recursive scanning.
|
depth (int): Depth level for recursive scanning.
|
||||||
"""
|
"""
|
||||||
if depth > 5:
|
if depth > self.depth:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for entry in sorted(directory.iterdir(), key=lambda e: (e.is_file(), e.name.lower())):
|
for entry in sorted(directory.iterdir(), key=lambda e: (e.is_file(), e.name.lower())):
|
||||||
if entry.is_dir():
|
if entry.is_dir():
|
||||||
if any(file.suffix == '.md' for file in entry.glob('*.md')):
|
self.process_directory(entry, parent_item, depth)
|
||||||
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)
|
|
||||||
elif entry.suffix == '.md':
|
elif entry.suffix == '.md':
|
||||||
file_item = QStandardItem(entry.name)
|
self.add_file_item(entry, parent_item)
|
||||||
file_item.setData(str(entry))
|
except PermissionError as e:
|
||||||
parent_item.appendRow(file_item)
|
logging.error(f"Permission error accessing {directory}: {e}")
|
||||||
except PermissionError:
|
|
||||||
pass
|
@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):
|
def on_directory_tree_clicked(self, index):
|
||||||
"""
|
"""
|
||||||
@ -655,6 +709,7 @@ class MarkdownEditor(QMainWindow):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
""" Main function to run the Markdown Editor application. """
|
""" Main function to run the Markdown Editor application. """
|
||||||
|
logging.info("Launching the application...")
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
editor = MarkdownEditor()
|
editor = MarkdownEditor()
|
||||||
editor.show()
|
editor.show()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user