DEV Community

Mate Technologies
Mate Technologies

Posted on

FileMate Pro – Next-Gen File Explorer in Python πŸ—‚οΈβœ¨

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()

Enter fullscreen mode Exit fullscreen mode

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)