Ever wished for a single-panel, lightweight, and modern file explorer built entirely in Python? Meet FileMate Pro, a next-gen file explorer with a lazy-loading folder tree, file previews, drag-and-drop, batch operations, bookmarks, and zip/unzip functionality β all in a sleek Tkinter interface.
Built with tkinter, PIL, and sv_ttk, FileMate Pro combines powerful file management with a clean, responsive UI.
Features
π³ Lazy Tree Sidebar β loads folders only when expanded.
πΌ File Preview β previews images and text files instantly.
π Single-Panel File Browser β intuitive navigation.
π File Operations β copy, paste, delete, batch rename.
π Bookmarks β save your favorite directories.
π¦ Zip/Unzip Support β easily compress or extract files.
π Dark Mode Toggle β easy on the eyes.
π Search & Sort β quickly locate files by name, type, or date.
Demo Screenshot
Clean, modern, and ready to manage your files efficiently.
The Full Python Script
FileMate_Pro_Next_Gen_Explorer.py
import os
import shutil
import threading
import zipfile
import tkinter as tk
from tkinter import ttk, simpledialog, filedialog, messagebox
from PIL import Image, ImageTk
import sv_ttk
# =========================
# App Setup
# =========================
root = tk.Tk()
root.title("FileMate Pro - Single Panel + Lazy Tree Sidebar")
root.geometry("1400x800")
sv_ttk.set_theme("light")
# =========================
# Globals
# =========================
clipboard = []
dark_mode_var = tk.BooleanVar(value=False)
search_var = tk.StringVar()
sort_var = tk.StringVar(value="Name")
status_var = tk.StringVar(value="Ready")
preview_image = None
bookmarks = []
def set_status(msg):
status_var.set(msg)
root.update_idletasks()
def toggle_theme():
sv_ttk.set_theme("dark" if dark_mode_var.get() else "light")
set_status(f"Theme switched to {'Dark' if dark_mode_var.get() else 'Light'} mode")
# =========================
# Single Tab File Panel
# =========================
class SingleTab(ttk.Frame):
def __init__(self, master, initial_path):
super().__init__(master)
self.current_path = initial_path
self.drag_data = {"items": None}
self.folder_sizes = {}
self.entries_cache = []
# Split root: tree sidebar + main panel
split_root = ttk.Frame(self)
split_root.pack(fill="both", expand=True)
# Folder tree sidebar
self.tree_frame = ttk.Frame(split_root, width=300)
self.tree_frame.pack(side="left", fill="y", padx=(10,0), pady=10)
self.tree_scroll = ttk.Scrollbar(self.tree_frame)
self.tree_scroll.pack(side="right", fill="y")
self.tree = ttk.Treeview(self.tree_frame, yscrollcommand=self.tree_scroll.set)
self.tree.pack(expand=True, fill="y")
self.tree_scroll.config(command=self.tree.yview)
self.tree.bind("<<TreeviewOpen>>", self.on_tree_expand)
self.tree.bind("<<TreeviewSelect>>", self.on_tree_select)
# Populate root tree
home_path = os.path.expanduser("~")
root_node = self.tree.insert("", "end", text=home_path, open=True, values=[home_path])
self.populate_tree(root_node, home_path)
# Main file panel
self.main_panel = ttk.Frame(split_root)
self.main_panel.pack(side="left", fill="both", expand=True)
# Toolbar
toolbar = ttk.Frame(self.main_panel)
toolbar.pack(fill="x", pady=(0,5))
ttk.Button(toolbar, text="Up", command=self.go_up).pack(side="left", padx=2)
ttk.Button(toolbar, text="New Folder", command=self.create_folder).pack(side="left", padx=2)
ttk.Button(toolbar, text="Delete", command=self.delete_items).pack(side="left", padx=2)
ttk.Button(toolbar, text="Copy", command=self.copy_items).pack(side="left", padx=2)
ttk.Button(toolbar, text="Paste", command=self.paste_items).pack(side="left", padx=2)
ttk.Button(toolbar, text="Batch Rename", command=self.batch_rename).pack(side="left", padx=2)
ttk.Button(toolbar, text="Add Bookmark", command=self.add_bookmark).pack(side="left", padx=2)
ttk.Button(toolbar, text="Zip Selected", command=self.zip_selected).pack(side="left", padx=2)
ttk.Button(toolbar, text="Unzip Selected", command=self.unzip_selected).pack(side="left", padx=2)
# Path Entry
path_frame = ttk.Frame(self.main_panel)
path_frame.pack(fill="x", pady=(0,5))
ttk.Label(path_frame, text="Path:").pack(side="left")
self.path_entry = ttk.Entry(path_frame)
self.path_entry.pack(side="left", fill="x", expand=True, padx=(2,2))
ttk.Button(path_frame, text="Go", command=self.go_to_path).pack(side="left", padx=2)
self.path_entry.insert(0, self.current_path)
# File list + preview split
split = ttk.Frame(self.main_panel)
split.pack(fill="both", expand=True)
# File List
self.file_listbox = tk.Listbox(split, selectmode=tk.EXTENDED, font=("Segoe UI", 11))
self.file_listbox.pack(side="left", fill="both", expand=True)
self.file_listbox.bind("<Double-Button-1>", lambda e: self.open_selected())
self.file_listbox.bind("<ButtonPress-1>", self.on_start_drag)
self.file_listbox.bind("<ButtonRelease-1>", self.on_drop)
self.file_listbox.bind("<<ListboxSelect>>", lambda e: self.preview_selected())
scrollbar = ttk.Scrollbar(split, orient="vertical", command=self.file_listbox.yview)
scrollbar.pack(side="left", fill="y")
self.file_listbox.config(yscrollcommand=scrollbar.set)
# Preview Panel
self.preview_panel = tk.Text(split, width=40, state="disabled")
self.preview_panel.pack(side="right", fill="both")
# Load initial folder
self.load_folder(self.current_path)
# -------------------------
# Tree Functions (Lazy)
# -------------------------
def populate_tree(self, parent, path):
try:
for item in os.listdir(path):
abs_path = os.path.join(path, item)
if os.path.isdir(abs_path):
node = self.tree.insert(parent, "end", text=item, values=[abs_path])
# dummy child for lazy expand
self.tree.insert(node, "end", text="dummy")
except PermissionError:
pass
def on_tree_expand(self, event):
node = self.tree.focus()
children = self.tree.get_children(node)
for child in children:
if self.tree.item(child, "text") == "dummy":
self.tree.delete(child)
path = self.tree.item(node, "values")[0]
self.populate_tree(node, path)
def on_tree_select(self, event):
node = self.tree.focus()
path = self.tree.item(node, "values")[0]
self.load_folder(path)
# -------------------------
# Load Folder Threaded
# -------------------------
def load_folder(self, path):
self.current_path = path
self.path_entry.delete(0, tk.END)
self.path_entry.insert(0, path)
self.file_listbox.delete(0, tk.END)
set_status(f"Loading {path}...")
threading.Thread(target=self._thread_load_folder, daemon=True).start()
def _thread_load_folder(self):
try:
entries = []
for item in os.listdir(self.current_path):
full_path = os.path.join(self.current_path, item)
is_dir = os.path.isdir(full_path)
ext = os.path.splitext(item)[1].lower() if not is_dir else ""
mtime = os.path.getmtime(full_path)
entries.append({"name": item, "path": full_path, "is_dir": is_dir, "ext": ext, "mtime": mtime})
self.entries_cache = entries
self.update_ui()
# compute folder sizes asynchronously
for e in entries:
if e["is_dir"]:
threading.Thread(target=self.compute_folder_size, args=(e["path"],), daemon=True).start()
except PermissionError:
messagebox.showerror("Error", "Permission denied")
def update_ui(self):
def _update():
self.file_listbox.delete(0, tk.END)
search_text = search_var.get().lower()
entries = self.sort_entries(self.entries_cache)
for e in entries:
if search_text in e["name"].lower():
icon = "π " if e["is_dir"] else "π "
size_str = ""
if e["is_dir"] and e["path"] in self.folder_sizes:
size_str = f" [{self.folder_sizes[e['path']]}]"
self.file_listbox.insert(tk.END, icon + e["name"] + size_str)
set_status(f"Loaded {len(entries)} items in {self.current_path}")
root.after(0, _update)
def sort_entries(self, entries):
method = sort_var.get()
if method == "Name":
return sorted(entries, key=lambda x: x["name"].lower())
elif method == "Type":
return sorted(entries, key=lambda x: (x["ext"], x["name"].lower()))
elif method == "Date":
return sorted(entries, key=lambda x: x["mtime"], reverse=True)
return entries
# -------------------------
# Folder Size
# -------------------------
def compute_folder_size(self, folder_path):
total = 0
for root_dir, dirs, files in os.walk(folder_path):
for f in files:
try:
total += os.path.getsize(os.path.join(root_dir, f))
except:
pass
size_str = self.human_readable_size(total)
self.folder_sizes[folder_path] = size_str
self.update_ui()
@staticmethod
def human_readable_size(size, decimal_places=2):
for unit in ['B','KB','MB','GB','TB']:
if size < 1024.0:
return f"{size:.{decimal_places}f} {unit}"
size /= 1024.0
return f"{size:.{decimal_places}f} PB"
# -------------------------
# Navigation & File Operations
# -------------------------
def go_up(self):
self.load_folder(os.path.dirname(self.current_path))
def go_to_path(self):
path = self.path_entry.get()
if os.path.exists(path):
self.load_folder(path)
else:
messagebox.showerror("Error", "Path does not exist!")
def get_selected_items(self):
return [os.path.join(self.current_path, self.file_listbox.get(idx)[2:].split(" [")[0])
for idx in self.file_listbox.curselection()]
def open_selected(self):
items = self.get_selected_items()
for path in items:
if os.path.isdir(path):
self.load_folder(path)
break
else:
try:
os.startfile(path)
set_status(f"Opened {os.path.basename(path)}")
except Exception as e:
messagebox.showerror("Error", str(e))
# -------------------------
# Drag & Drop
# -------------------------
def on_start_drag(self, event):
selection = self.file_listbox.curselection()
if selection:
self.drag_data["items"] = self.get_selected_items()
def on_drop(self, event):
if not self.drag_data["items"]:
return
index = self.file_listbox.nearest(event.y)
dest_name = self.file_listbox.get(index)[2:].split(" [")[0]
dest_path = os.path.join(self.current_path, dest_name)
if not os.path.isdir(dest_path):
return
for item in self.drag_data["items"]:
try:
shutil.move(item, os.path.join(dest_path, os.path.basename(item)))
except Exception as e:
messagebox.showerror("Error", str(e))
self.drag_data["items"] = None
self.load_folder(self.current_path)
set_status("Moved items successfully")
def copy_items(self):
global clipboard
clipboard = self.get_selected_items()
set_status(f"Copied {len(clipboard)} items")
def paste_items(self):
global clipboard
if not clipboard:
return
for item in clipboard:
dest = os.path.join(self.current_path, os.path.basename(item))
try:
if os.path.isdir(item):
shutil.copytree(item, dest)
else:
shutil.copy2(item, dest)
except Exception as e:
messagebox.showerror("Error", str(e))
self.load_folder(self.current_path)
set_status(f"Pasted {len(clipboard)} items")
# -------------------------
# Folder/File Operations
# -------------------------
def create_folder(self):
name = simpledialog.askstring("Create Folder", "Enter folder name:")
if name:
path = os.path.join(self.current_path, name)
try:
os.makedirs(path)
self.load_folder(self.current_path)
set_status(f"Created folder {name}")
except Exception as e:
messagebox.showerror("Error", str(e))
def delete_items(self):
items = self.get_selected_items()
if not items:
return
if messagebox.askyesno("Confirm Delete", f"Delete {len(items)} items?"):
for path in items:
try:
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
except Exception as e:
messagebox.showerror("Error", str(e))
self.load_folder(self.current_path)
set_status("Deleted items")
def batch_rename(self):
items = self.get_selected_items()
if not items:
return
pattern = simpledialog.askstring("Batch Rename", "Enter pattern with {num}, e.g., file_{num}.txt")
if not pattern:
return
for i, path in enumerate(items, 1):
dir_path = os.path.dirname(path)
ext = os.path.splitext(path)[1]
new_name = pattern.replace("{num}", str(i))
if not new_name.endswith(ext):
new_name += ext
new_path = os.path.join(dir_path, new_name)
try:
os.rename(path, new_path)
except Exception as e:
messagebox.showerror("Error", str(e))
self.load_folder(self.current_path)
set_status(f"Batch renamed {len(items)} items")
def add_bookmark(self):
global bookmarks
if self.current_path not in bookmarks:
bookmarks.append(self.current_path)
set_status(f"Bookmarked {self.current_path}")
else:
set_status(f"Already bookmarked")
def zip_selected(self):
items = self.get_selected_items()
if not items:
return
zip_path = filedialog.asksaveasfilename(defaultextension=".zip")
if not zip_path:
return
with zipfile.ZipFile(zip_path, "w") as zf:
for item in items:
if os.path.isdir(item):
for root_dir, dirs, files in os.walk(item):
for f in files:
full_path = os.path.join(root_dir, f)
arcname = os.path.relpath(full_path, os.path.dirname(item))
zf.write(full_path, arcname)
else:
zf.write(item, os.path.basename(item))
self.load_folder(self.current_path)
set_status(f"Created archive {zip_path}")
def unzip_selected(self):
items = self.get_selected_items()
if not items:
return
for item in items:
if zipfile.is_zipfile(item):
with zipfile.ZipFile(item, "r") as zf:
zf.extractall(self.current_path)
self.load_folder(self.current_path)
set_status("Unzipped selected archives")
# -------------------------
# Preview Panel
# -------------------------
def preview_selected(self):
global preview_image
items = self.get_selected_items()
if not items:
self.preview_panel.config(state="normal")
self.preview_panel.delete(1.0, tk.END)
self.preview_panel.config(state="disabled")
return
path = items[0]
self.preview_panel.config(state="normal")
self.preview_panel.delete(1.0, tk.END)
if os.path.isdir(path):
self.preview_panel.insert(tk.END, f"Folder: {path}\n")
try:
self.preview_panel.insert(tk.END, f"Contains: {len(os.listdir(path))} items")
except:
self.preview_panel.insert(tk.END, "Cannot read folder contents")
else:
ext = os.path.splitext(path)[1].lower()
if ext in [".png",".jpg",".jpeg",".gif",".bmp"]:
try:
img = Image.open(path)
img.thumbnail((400,400))
preview_image = ImageTk.PhotoImage(img)
self.preview_panel.image_create(tk.END, image=preview_image)
except:
self.preview_panel.insert(tk.END, "Cannot preview image.")
else:
try:
with open(path,"r",encoding="utf-8") as f:
content = f.read(1000)
self.preview_panel.insert(tk.END, content)
except:
self.preview_panel.insert(tk.END, "No preview available.")
self.preview_panel.config(state="disabled")
# =========================
# Layout: Single Tab
# =========================
pane = SingleTab(root, os.path.expanduser("~"))
pane.pack(fill="both", expand=True, padx=10, pady=10)
# Search & Sort
search_frame = ttk.Frame(root)
search_frame.pack(fill="x", padx=10)
ttk.Label(search_frame, text="Search:").pack(side="left")
ttk.Entry(search_frame, textvariable=search_var).pack(side="left", fill="x", expand=True, padx=5)
search_var.trace("w", lambda *a: pane.update_ui())
ttk.Label(search_frame, text="Sort by:").pack(side="left", padx=(10,0))
ttk.Combobox(search_frame, textvariable=sort_var, values=["Name","Type","Date"], state="readonly", width=10).pack(side="left")
sort_var.trace("w", lambda *a: pane.update_ui())
ttk.Checkbutton(search_frame, text="Dark Mode", variable=dark_mode_var, command=toggle_theme).pack(side="right", padx=5)
# Status Bar
ttk.Label(root, textvariable=status_var, anchor="w", font=("Segoe UI", 10)).pack(side="bottom", fill="x")
# =========================
# Run
# =========================
root.mainloop()
FileMate Pro: A modern, single-panel file explorer with lazy-loading, drag-and-drop, file previews, bookmarks, batch operations, and dark mode β entirely in Python using Tkinter and PIL.
Top comments (0)