File Search Engine
This application is a local file search engine that uses threading and a queue
to manage IO tasks in order to keep the gui interactive. The Treeview
updates
the results in real-time and sets the focus and view on the most recently
inserted result in the results Treeview
.
Style Summary
The theme used in this example is journal.
Item | Class | Bootstyle |
---|---|---|
User Input | Entry |
default |
Browse | Button |
primary |
Search | Button |
primary-outline |
Treeview | Treeview |
info |
Progressbar | Progressbar |
success |
Example Code
Run this code live on repl.it
import datetime
import pathlib
from queue import Queue
from threading import Thread
from tkinter.filedialog import askdirectory
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap import utility
class FileSearchEngine(ttk.Frame):
queue = Queue()
searching = False
def __init__(self, master):
super().__init__(master, padding=15)
self.pack(fill=BOTH, expand=YES)
# application variables
_path = pathlib.Path().absolute().as_posix()
self.path_var = ttk.StringVar(value=_path)
self.term_var = ttk.StringVar(value='md')
self.type_var = ttk.StringVar(value='endswidth')
# header and labelframe option container
option_text = "Complete the form to begin your search"
self.option_lf = ttk.Labelframe(self, text=option_text, padding=15)
self.option_lf.pack(fill=X, expand=YES, anchor=N)
self.create_path_row()
self.create_term_row()
self.create_type_row()
self.create_results_view()
self.progressbar = ttk.Progressbar(
master=self,
mode=INDETERMINATE,
bootstyle=(STRIPED, SUCCESS)
)
self.progressbar.pack(fill=X, expand=YES)
def create_path_row(self):
"""Add path row to labelframe"""
path_row = ttk.Frame(self.option_lf)
path_row.pack(fill=X, expand=YES)
path_lbl = ttk.Label(path_row, text="Path", width=8)
path_lbl.pack(side=LEFT, padx=(15, 0))
path_ent = ttk.Entry(path_row, textvariable=self.path_var)
path_ent.pack(side=LEFT, fill=X, expand=YES, padx=5)
browse_btn = ttk.Button(
master=path_row,
text="Browse",
command=self.on_browse,
width=8
)
browse_btn.pack(side=LEFT, padx=5)
def create_term_row(self):
"""Add term row to labelframe"""
term_row = ttk.Frame(self.option_lf)
term_row.pack(fill=X, expand=YES, pady=15)
term_lbl = ttk.Label(term_row, text="Term", width=8)
term_lbl.pack(side=LEFT, padx=(15, 0))
term_ent = ttk.Entry(term_row, textvariable=self.term_var)
term_ent.pack(side=LEFT, fill=X, expand=YES, padx=5)
search_btn = ttk.Button(
master=term_row,
text="Search",
command=self.on_search,
bootstyle=OUTLINE,
width=8
)
search_btn.pack(side=LEFT, padx=5)
def create_type_row(self):
"""Add type row to labelframe"""
type_row = ttk.Frame(self.option_lf)
type_row.pack(fill=X, expand=YES)
type_lbl = ttk.Label(type_row, text="Type", width=8)
type_lbl.pack(side=LEFT, padx=(15, 0))
contains_opt = ttk.Radiobutton(
master=type_row,
text="Contains",
variable=self.type_var,
value="contains"
)
contains_opt.pack(side=LEFT)
startswith_opt = ttk.Radiobutton(
master=type_row,
text="StartsWith",
variable=self.type_var,
value="startswith"
)
startswith_opt.pack(side=LEFT, padx=15)
endswith_opt = ttk.Radiobutton(
master=type_row,
text="EndsWith",
variable=self.type_var,
value="endswith"
)
endswith_opt.pack(side=LEFT)
endswith_opt.invoke()
def create_results_view(self):
"""Add result treeview to labelframe"""
self.resultview = ttk.Treeview(
master=self,
bootstyle=INFO,
columns=[0, 1, 2, 3, 4],
show=HEADINGS
)
self.resultview.pack(fill=BOTH, expand=YES, pady=10)
# setup columns and use `scale_size` to adjust for resolution
self.resultview.heading(0, text='Name', anchor=W)
self.resultview.heading(1, text='Modified', anchor=W)
self.resultview.heading(2, text='Type', anchor=E)
self.resultview.heading(3, text='Size', anchor=E)
self.resultview.heading(4, text='Path', anchor=W)
self.resultview.column(
column=0,
anchor=W,
width=utility.scale_size(self, 125),
stretch=False
)
self.resultview.column(
column=1,
anchor=W,
width=utility.scale_size(self, 140),
stretch=False
)
self.resultview.column(
column=2,
anchor=E,
width=utility.scale_size(self, 50),
stretch=False
)
self.resultview.column(
column=3,
anchor=E,
width=utility.scale_size(self, 50),
stretch=False
)
self.resultview.column(
column=4,
anchor=W,
width=utility.scale_size(self, 300)
)
def on_browse(self):
"""Callback for directory browse"""
path = askdirectory(title="Browse directory")
if path:
self.path_var.set(path)
def on_search(self):
"""Search for a term based on the search type"""
search_term = self.term_var.get()
search_path = self.path_var.get()
search_type = self.type_var.get()
if search_term == '':
return
# start search in another thread to prevent UI from locking
Thread(
target=FileSearchEngine.file_search,
args=(search_term, search_path, search_type),
daemon=True
).start()
self.progressbar.start(10)
iid = self.resultview.insert(
parent='',
index=END,
)
self.resultview.item(iid, open=True)
self.after(100, lambda: self.check_queue(iid))
def check_queue(self, iid):
"""Check file queue and print results if not empty"""
if all([
FileSearchEngine.searching,
not FileSearchEngine.queue.empty()
]):
filename = FileSearchEngine.queue.get()
self.insert_row(filename, iid)
self.update_idletasks()
self.after(100, lambda: self.check_queue(iid))
elif all([
not FileSearchEngine.searching,
not FileSearchEngine.queue.empty()
]):
while not FileSearchEngine.queue.empty():
filename = FileSearchEngine.queue.get()
self.insert_row(filename, iid)
self.update_idletasks()
self.progressbar.stop()
elif all([
FileSearchEngine.searching,
FileSearchEngine.queue.empty()
]):
self.after(100, lambda: self.check_queue(iid))
else:
self.progressbar.stop()
def insert_row(self, file, iid):
"""Insert new row in tree search results"""
try:
_stats = file.stat()
_name = file.stem
_timestamp = datetime.datetime.fromtimestamp(_stats.st_mtime)
_modified = _timestamp.strftime(r'%m/%d/%Y %I:%M:%S%p')
_type = file.suffix.lower()
_size = FileSearchEngine.convert_size(_stats.st_size)
_path = file.as_posix()
iid = self.resultview.insert(
parent='',
index=END,
values=(_name, _modified, _type, _size, _path)
)
self.resultview.selection_set(iid)
self.resultview.see(iid)
except OSError:
return
@staticmethod
def file_search(term, search_path, search_type):
"""Recursively search directory for matching files"""
FileSearchEngine.set_searching(1)
if search_type == 'contains':
FileSearchEngine.find_contains(term, search_path)
elif search_type == 'startswith':
FileSearchEngine.find_startswith(term, search_path)
elif search_type == 'endswith':
FileSearchEngine.find_endswith(term, search_path)
@staticmethod
def find_contains(term, search_path):
"""Find all files that contain the search term"""
for path, _, files in pathlib.os.walk(search_path):
if files:
for file in files:
if term in file:
record = pathlib.Path(path) / file
FileSearchEngine.queue.put(record)
FileSearchEngine.set_searching(False)
@staticmethod
def find_startswith(term, search_path):
"""Find all files that start with the search term"""
for path, _, files in pathlib.os.walk(search_path):
if files:
for file in files:
if file.startswith(term):
record = pathlib.Path(path) / file
FileSearchEngine.queue.put(record)
FileSearchEngine.set_searching(False)
@staticmethod
def find_endswith(term, search_path):
"""Find all files that end with the search term"""
for path, _, files in pathlib.os.walk(search_path):
if files:
for file in files:
if file.endswith(term):
record = pathlib.Path(path) / file
FileSearchEngine.queue.put(record)
FileSearchEngine.set_searching(False)
@staticmethod
def set_searching(state=False):
"""Set searching status"""
FileSearchEngine.searching = state
@staticmethod
def convert_size(size):
"""Convert bytes to mb or kb depending on scale"""
kb = size // 1000
mb = round(kb / 1000, 1)
if kb > 1000:
return f'{mb:,.1f} MB'
else:
return f'{kb:,d} KB'
if __name__ == '__main__':
app = ttk.Window("File Search Engine", "journal")
FileSearchEngine(app)
app.mainloop()