
Kod: Tümünü seç
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# =============================================================================
# Project : VB6 -> Python Conversion
# Module : File Explorer (Section 10)
# Platform : GNU/Linux Mint 22.2 MATE x64 | Windows 11 24H2 x64
# Python : 3.8+ (stdlib only)
# Fix v1.2 : - Context menu auto-dismisses on left-click / Refresh
# - Root (/) and C:\ visible; system paths read-only
# - Other mount points (D:\ /media /mnt) fully unrestricted
# - Virtual FS (/proc /sys /dev /run) shown but not traversed
# =============================================================================
import os
import sys
# ---------------------------------------------------------------------------
# VENV BOOTSTRAPPER (v1.1 - robust)
# ---------------------------------------------------------------------------
VENV_DIR = "/home/linuxmaster/python-toolchain"
_SENTINEL_FLAG = "_VENV_BOOTSTRAP_DONE"
_candidates = [
os.path.join(VENV_DIR, "bin", "python3"),
os.path.join(VENV_DIR, "bin", "python"),
os.path.join(VENV_DIR, "Scripts", "python.exe"),
]
def _venv_is_healthy():
lib = os.path.join(VENV_DIR, "lib")
if os.path.isdir(lib):
for e in os.listdir(lib):
if os.path.isdir(os.path.join(lib, e, "site-packages")):
return True
return os.path.isdir(os.path.join(VENV_DIR, "Lib", "site-packages"))
def _bootstrap_venv():
if os.environ.get(_SENTINEL_FLAG) == "1":
return
inside = os.path.normcase(os.path.realpath(sys.executable)).startswith(
os.path.normcase(os.path.realpath(VENV_DIR)))
if inside or not _venv_is_healthy():
return
for c in _candidates:
if os.path.isfile(c):
os.environ[_SENTINEL_FLAG] = "1"
os.execv(c, [c] + sys.argv)
_bootstrap_venv()
# ---------------------------------------------------------------------------
import shutil, fnmatch, string, datetime
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog, filedialog
IS_WINDOWS = sys.platform.startswith("win")
# ---------------------------------------------------------------------------
# VIRTUAL FILESYSTEMS (Linux) — visible in tree but NOT traversed/listed
# ---------------------------------------------------------------------------
_LINUX_VFS = {"/proc", "/sys", "/dev", "/run"}
def _is_virtual_fs(path: str) -> bool:
if IS_WINDOWS:
return False
real = os.path.realpath(path)
for vfs in _LINUX_VFS:
if real == vfs or real.startswith(vfs + "/"):
return True
return False
# ---------------------------------------------------------------------------
# PROTECTION RULES
# ---------------------------------------------------------------------------
# Linux: / and its direct system subdirs are protected.
# /home/<current_user>/ and everything under it = FREE
# /media/* and /mnt/* = FREE
# Windows: C:\ system dirs protected, D:\ E:\ ... = fully FREE
_LINUX_PROTECTED_DIRS = {
"/bin", "/sbin", "/usr", "/lib", "/lib32", "/lib64", "/libx32",
"/boot", "/etc", "/opt", "/root", "/snap", "/srv", "/tmp", "/var",
"/proc", "/sys", "/dev", "/run",
}
_WINDOWS_PROTECTED_DIRS = {
"windows", "system32", "syswow64", "program files",
"program files (x86)", "programdata",
}
HOME = os.path.realpath(os.path.expanduser("~"))
def _is_protected(path: str) -> bool:
real = os.path.realpath(path)
if IS_WINDOWS:
# Only C:\ drive is subject to protection rules
drive = os.path.splitdrive(real)[0].upper()
if drive != "C:":
return False # D:\ E:\ etc. — fully free
# Anything directly inside C:\Windows, C:\Program Files, etc.
parts_lower = real.lower().replace("\\", "/").split("/")
# parts_lower[0] = "c:", parts_lower[1] = top-level dir
if len(parts_lower) >= 2:
top = parts_lower[1] if len(parts_lower) > 1 else ""
if top in _WINDOWS_PROTECTED_DIRS:
return True
# Also protect C:\ itself from direct file operations
if real.lower() in ("c:\\", "c:/", "c:"):
return True
return False
else:
# Always free: home dir and everything inside it
if real == HOME or real.startswith(HOME + "/"):
return False
# Always free: /media and /mnt (removable / external)
if real.startswith("/media/") or real.startswith("/mnt/"):
return False
# Root itself and all system subdirs are protected
if real == "/":
return True
for pdir in _LINUX_PROTECTED_DIRS:
if real == pdir or real.startswith(pdir + "/"):
return True
return False
# ---------------------------------------------------------------------------
# MISC HELPERS
# ---------------------------------------------------------------------------
WIN_HIDDEN = 0x02
def is_hidden(name: str, full: str) -> bool:
if IS_WINDOWS:
try:
import ctypes
a = ctypes.windll.kernel32.GetFileAttributesW(full)
return bool(a != -1 and a & WIN_HIDDEN)
except Exception:
return False
return name.startswith(".")
def get_roots() -> list:
if IS_WINDOWS:
return [f"{l}:\\" for l in string.ascii_uppercase if os.path.exists(f"{l}:\\")]
else:
roots = ["/", HOME]
try:
with open("/proc/mounts") as f:
for line in f:
p = line.split()
if len(p) >= 2 and (p[1].startswith("/media") or p[1].startswith("/mnt")):
if os.path.isdir(p[1]):
roots.append(p[1])
except OSError:
pass
return roots
FILTER_OPTIONS = ["*.*","*.txt","*.py","*.sh","*.jpg","*.png",
"*.pdf","*.mp3","*.mp4","*.zip","*.tar.gz"]
TOOLBAR_BG = "#f0f0f0"
STATUS_BG = "#e8e8e8"
# ===========================================================================
# MAIN APP
# ===========================================================================
class FileExplorerApp:
def __init__(self, master: tk.Tk):
self.master = master
self._roots = get_roots()
self._cur_dir = HOME
self._tree_nodes = {}
self._ctx_active = False # tracks if context menu is posted
master.title("File Explorer")
master.minsize(700, 500)
self._build_toolbar()
self._build_filter_bar()
self._build_panes()
self._build_status_bar()
self._build_ctx_menu()
self._populate_tree(self._roots[0])
self._list_files(self._cur_dir)
self._center_window()
master.protocol("WM_DELETE_WINDOW", self._on_quit)
# ------------------------------------------------------------------
# TOOLBAR
# ------------------------------------------------------------------
def _build_toolbar(self):
tb = tk.Frame(self.master, bg=TOOLBAR_BG, relief=tk.RAISED, bd=1)
tb.pack(fill=tk.X)
for lbl, cmd in [
("New File", self._op_new_file),
("New Folder", self._op_new_folder),
("Delete", self._op_delete),
("Copy", self._op_copy),
("Rename", self._op_rename),
("Refresh", self._op_refresh),
("Help", self._op_help),
]:
tk.Button(tb, text=lbl, width=9, relief=tk.GROOVE,
bg=TOOLBAR_BG, activebackground="#d0d0d0",
command=cmd).pack(side=tk.LEFT, padx=3, pady=3)
# ------------------------------------------------------------------
# FILTER BAR
# ------------------------------------------------------------------
def _build_filter_bar(self):
bar = tk.Frame(self.master, bg=TOOLBAR_BG)
bar.pack(fill=tk.X, padx=4, pady=2)
tk.Label(bar, text="Drive / Root:", bg=TOOLBAR_BG).pack(side=tk.LEFT)
self.combo_drive = ttk.Combobox(bar, values=self._roots,
width=22, state="readonly")
self.combo_drive.current(0)
self.combo_drive.pack(side=tk.LEFT, padx=(2, 16))
self.combo_drive.bind("<<ComboboxSelected>>", self._on_drive_change)
tk.Label(bar, text="Filter:", bg=TOOLBAR_BG).pack(side=tk.LEFT)
self.combo_filter = ttk.Combobox(bar, values=FILTER_OPTIONS, width=12)
self.combo_filter.current(0)
self.combo_filter.pack(side=tk.LEFT, padx=2)
self.combo_filter.bind("<<ComboboxSelected>>", self._on_filter_change)
self.combo_filter.bind("<Return>", self._on_filter_change)
# ------------------------------------------------------------------
# PANES
# ------------------------------------------------------------------
def _build_panes(self):
paned = tk.PanedWindow(self.master, orient=tk.HORIZONTAL,
sashwidth=5, sashrelief=tk.RAISED)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=2)
# LEFT
lf = tk.Frame(paned, relief=tk.SUNKEN, bd=1)
tk.Label(lf, text="Folders", anchor="w", bg="#d4d0c8",
font=("Helvetica", 9, "bold")).pack(fill=tk.X)
sy = tk.Scrollbar(lf); sy.pack(side=tk.RIGHT, fill=tk.Y)
sx = tk.Scrollbar(lf, orient=tk.HORIZONTAL)
sx.pack(side=tk.BOTTOM, fill=tk.X)
self.dir_tree = ttk.Treeview(lf, yscrollcommand=sy.set,
xscrollcommand=sx.set,
show="tree", selectmode="browse")
self.dir_tree.pack(fill=tk.BOTH, expand=True)
sy.config(command=self.dir_tree.yview)
sx.config(command=self.dir_tree.xview)
self.dir_tree.bind("<<TreeviewSelect>>", self._on_tree_select)
self.dir_tree.bind("<<TreeviewOpen>>", self._on_tree_expand)
self.dir_tree.bind("<Button-1>", self._dismiss_ctx)
paned.add(lf, minsize=180)
# RIGHT
rf = tk.Frame(paned, relief=tk.SUNKEN, bd=1)
tk.Label(rf, text="Files", anchor="w", bg="#d4d0c8",
font=("Helvetica", 9, "bold")).pack(fill=tk.X)
sfy = tk.Scrollbar(rf); sfy.pack(side=tk.RIGHT, fill=tk.Y)
self.file_list = tk.Listbox(rf, yscrollcommand=sfy.set,
selectbackground="#0078d7",
selectforeground="#ffffff",
font=("Courier", 10), activestyle="none")
self.file_list.pack(fill=tk.BOTH, expand=True)
sfy.config(command=self.file_list.yview)
self.file_list.bind("<Double-Button-1>", self._on_dbl_click)
self.file_list.bind("<Button-3>", self._show_ctx)
# Left-click on empty area dismisses context menu
self.file_list.bind("<Button-1>", self._on_list_left_click)
paned.add(rf, minsize=200)
# ------------------------------------------------------------------
# STATUS BAR
# ------------------------------------------------------------------
def _build_status_bar(self):
bot = tk.Frame(self.master, relief=tk.SUNKEN, bd=1, bg=STATUS_BG)
bot.pack(fill=tk.X, side=tk.BOTTOM)
tk.Button(bot, text="Quit", width=8, fg="#cc0000",
activeforeground="#ffffff", activebackground="#cc0000",
command=self._on_quit).pack(side=tk.RIGHT, padx=4, pady=2)
self.lbl_status = tk.Label(bot, text=" Ready.", anchor="w",
bg=STATUS_BG, font=("Courier", 9))
self.lbl_status.pack(side=tk.LEFT, fill=tk.X, expand=True)
# ------------------------------------------------------------------
# CONTEXT MENU
# ------------------------------------------------------------------
def _build_ctx_menu(self):
self.ctx = tk.Menu(self.master, tearoff=0)
self.ctx.add_command(label="Open", command=self._ctx_open)
self.ctx.add_separator()
self.ctx.add_command(label="Copy", command=self._op_copy)
self.ctx.add_command(label="Rename", command=self._op_rename)
self.ctx.add_command(label="Delete", command=self._op_delete)
self.ctx.add_separator()
self.ctx.add_command(label="Properties", command=self._ctx_props)
def _show_ctx(self, event: tk.Event):
"""Show context menu only when pointer is over an actual item."""
idx = self.file_list.nearest(event.y)
if idx < 0 or idx >= self.file_list.size():
return
# Verify the click is really on the item (not below last item)
bbox = self.file_list.bbox(idx)
if not bbox:
return
self.file_list.selection_clear(0, tk.END)
self.file_list.selection_set(idx)
self._ctx_active = True
try:
self.ctx.tk_popup(event.x_root, event.y_root)
finally:
self.ctx.grab_release()
def _dismiss_ctx(self, _event=None):
"""Dismiss context menu and clear file list selection."""
if self._ctx_active:
try:
self.ctx.unpost()
except Exception:
pass
self._ctx_active = False
def _on_list_left_click(self, event: tk.Event):
"""Left-click: dismiss menu; deselect if click is on empty area."""
self._dismiss_ctx()
idx = self.file_list.nearest(event.y)
if idx >= 0:
bbox = self.file_list.bbox(idx)
if bbox and event.y > bbox[1] + bbox[3]:
self.file_list.selection_clear(0, tk.END)
# ------------------------------------------------------------------
# DIRECTORY TREE
# ------------------------------------------------------------------
def _populate_tree(self, root_path: str):
self.dir_tree.delete(*self.dir_tree.get_children())
self._tree_nodes.clear()
rid = self.dir_tree.insert("", "end", text=root_path, open=True)
self._tree_nodes[rid] = root_path
self._insert_subdirs(rid, root_path)
def _insert_subdirs(self, parent_id: str, path: str):
try:
entries = sorted(os.listdir(path))
except (PermissionError, OSError):
return
for name in entries:
full = os.path.join(path, name)
if not os.path.isdir(full):
continue
if is_hidden(name, full):
continue
child = self.dir_tree.insert(parent_id, "end", text=name)
self._tree_nodes[child] = full
# Virtual FS: show but never expand (prevents hang)
if _is_virtual_fs(full):
continue
if self._has_subdirs(full):
self.dir_tree.insert(child, "end", text="...", tags=("dummy",))
def _has_subdirs(self, path: str) -> bool:
if _is_virtual_fs(path):
return False
try:
for name in os.listdir(path):
full = os.path.join(path, name)
if os.path.isdir(full) and not is_hidden(name, full):
return True
except (PermissionError, OSError):
pass
return False
def _on_tree_expand(self, _event):
node = self.dir_tree.focus()
children = self.dir_tree.get_children(node)
if len(children) == 1 and self.dir_tree.item(children[0], "tags") == ("dummy",):
self.dir_tree.delete(children[0])
path = self._tree_nodes.get(node, "")
if path:
self._insert_subdirs(node, path)
def _on_tree_select(self, _event):
node = self.dir_tree.focus()
path = self._tree_nodes.get(node)
if not path or not os.path.isdir(path):
return
if _is_virtual_fs(path):
self._set_status(f" [Virtual FS] {path} — cannot browse safely.")
return
self._cur_dir = path
self._list_files(path)
# ------------------------------------------------------------------
# FILE LIST
# ------------------------------------------------------------------
def _list_files(self, path: str):
self.file_list.delete(0, tk.END)
if _is_virtual_fs(path):
self._set_status(f" [Virtual FS] {path} — skipped.")
return
pattern = self.combo_filter.get().strip() or "*.*"
dir_count = file_count = hidden = 0
try:
entries = sorted(os.listdir(path))
except (PermissionError, OSError):
self._set_status(" Permission denied.")
return
# ---- Directories first ----
for name in entries:
full = os.path.join(path, name)
if not os.path.isdir(full):
continue
if _is_virtual_fs(full):
continue
if is_hidden(name, full):
hidden += 1
continue
self.file_list.insert(tk.END, f"[DIR] {name}")
self.file_list.itemconfig(tk.END, fg="#0055aa")
dir_count += 1
# ---- Files (filtered by pattern) ----
for name in entries:
full = os.path.join(path, name)
if not os.path.isfile(full):
continue
if is_hidden(name, full):
hidden += 1
continue
if not fnmatch.fnmatch(name.lower(), pattern.lower()):
hidden += 1
continue
self.file_list.insert(tk.END, name)
file_count += 1
lock = " [READ-ONLY]" if _is_protected(path) else ""
self._set_status(
f" {path}{lock} | "
f"{dir_count} folder(s) {file_count} file(s) "
f"[{hidden} hidden/filtered]"
)
# ------------------------------------------------------------------
# EVENTS
# ------------------------------------------------------------------
def _on_drive_change(self, _event):
root = self.combo_drive.get()
self._cur_dir = root
self._populate_tree(root)
self._list_files(root)
def _on_filter_change(self, _event):
self._list_files(self._cur_dir)
def _on_dbl_click(self, _event):
path = self._sel_path()
if not path:
return
if os.path.isdir(path):
if _is_virtual_fs(path):
self._set_status(f" [Virtual FS] {path} — cannot browse.")
return
self._cur_dir = path
self._list_files(path)
else:
self._open_path(path)
def _ctx_open(self):
path = self._sel_path()
if path:
self._open_path(path)
@staticmethod
def _open_path(path: str):
try:
if IS_WINDOWS:
os.startfile(path)
else:
os.system(f'xdg-open "{path}" &')
except Exception as exc:
messagebox.showerror("Open Error", str(exc))
# ------------------------------------------------------------------
# PROTECTION GUARD
# ------------------------------------------------------------------
def _guard(self, path: str, action: str) -> bool:
if _is_protected(path):
messagebox.showerror(
"Access Denied",
f"Cannot {action}:\n\n{path}\n\n"
"This path is protected (system / critical directory).\n"
"You can VIEW files here, but not modify them.\n\n"
"Tip: Only your home folder and removable drives are writable.",
parent=self.master,
)
return True
return False
# ------------------------------------------------------------------
# HELPERS
# ------------------------------------------------------------------
def _sel_path(self) -> str | None:
sel = self.file_list.curselection()
if not sel:
return None
raw = self.file_list.get(sel[0])
name = raw[7:] if raw.startswith("[DIR] ") else raw # strip prefix
return os.path.join(self._cur_dir, name)
def _sel_is_dir(self) -> bool:
sel = self.file_list.curselection()
if not sel:
return False
return self.file_list.get(sel[0]).startswith("[DIR] ")
# ------------------------------------------------------------------
# TOOLBAR OPERATIONS
# ------------------------------------------------------------------
def _op_new_file(self):
if self._guard(self._cur_dir, "create files in"):
return
name = simpledialog.askstring("New File", "Enter new file name:",
parent=self.master)
if not name:
return
full = os.path.join(self._cur_dir, name.strip())
if os.path.exists(full):
messagebox.showwarning("New File", f'"{name}" already exists.',
parent=self.master)
return
try:
open(full, "w").close()
self._list_files(self._cur_dir)
self._set_status(f" Created: {full}")
except OSError as exc:
messagebox.showerror("New File Error", str(exc), parent=self.master)
def _op_new_folder(self):
if self._guard(self._cur_dir, "create folders in"):
return
name = simpledialog.askstring(
"New Folder", "Enter new folder name:", parent=self.master
)
if not name:
return
full = os.path.join(self._cur_dir, name.strip())
if os.path.exists(full):
messagebox.showwarning("New Folder", f'"{name}" already exists.',
parent=self.master)
return
try:
os.makedirs(full)
self._op_refresh()
self._set_status(f" Folder created: {full}")
except OSError as exc:
messagebox.showerror("New Folder Error", str(exc), parent=self.master)
def _op_delete(self):
path = self._sel_path()
if not path:
messagebox.showinfo("Delete", "No file selected.", parent=self.master)
return
if self._guard(path, "delete"):
return
name = os.path.basename(path)
if not messagebox.askyesno(
"Delete",
f'Permanently delete:\n"{name}"?\n\nThis cannot be undone.',
icon=messagebox.WARNING, parent=self.master):
return
try:
shutil.rmtree(path) if os.path.isdir(path) else os.remove(path)
self._list_files(self._cur_dir)
self._set_status(f" Deleted: {name}")
except OSError as exc:
messagebox.showerror("Delete Error", str(exc), parent=self.master)
def _op_copy(self):
path = self._sel_path()
if not path:
messagebox.showinfo("Copy", "No file selected.", parent=self.master)
return
dest_dir = filedialog.askdirectory(title="Copy to — select destination",
parent=self.master)
if not dest_dir:
return
if self._guard(dest_dir, "copy into"):
return
dest = os.path.join(dest_dir, os.path.basename(path))
try:
shutil.copy2(path, dest) if os.path.isfile(path) \
else shutil.copytree(path, dest)
self._set_status(f" Copied to: {dest_dir}")
except OSError as exc:
messagebox.showerror("Copy Error", str(exc), parent=self.master)
def _op_rename(self):
path = self._sel_path()
if not path:
messagebox.showinfo("Rename", "No file selected.", parent=self.master)
return
if self._guard(path, "rename"):
return
old = os.path.basename(path)
new = simpledialog.askstring("Rename", f'Rename "{old}" to:',
initialvalue=old, parent=self.master)
if not new or new.strip() == old:
return
dest = os.path.join(self._cur_dir, new.strip())
if os.path.exists(dest):
messagebox.showwarning("Rename", f'"{new}" already exists.',
parent=self.master)
return
try:
os.rename(path, dest)
self._list_files(self._cur_dir)
self._set_status(f' Renamed: "{old}" -> "{new.strip()}"')
except OSError as exc:
messagebox.showerror("Rename Error", str(exc), parent=self.master)
def _op_refresh(self):
self._dismiss_ctx()
self.file_list.selection_clear(0, tk.END)
# Rebuild tree only from the current drive root (preserves _cur_dir)
self._populate_tree(self.combo_drive.get())
# Re-list the currently open directory
self._list_files(self._cur_dir)
self._set_status(f" Refreshed: {self._cur_dir}")
def _op_help(self):
messagebox.showinfo(
"Help",
"File Explorer — Usage Guide\n\n"
"New File : Create an empty file in the current directory.\n"
"Delete : Permanently delete the selected file.\n"
"Copy : Copy selected file to a chosen directory.\n"
"Rename : Rename the selected file.\n"
"Refresh : Reload tree and file list; dismisses any open menu.\n\n"
"Left pane : Click a folder to browse its contents.\n"
"Right pane : Double-click a file to open it.\n"
" Right-click a file for more options.\n"
" Left-click on empty area dismisses the menu.\n\n"
"PROTECTION RULES\n"
"Linux : / and all system directories are READ-ONLY.\n"
" Your home folder and /media /mnt are fully writable.\n"
"Windows: C:\\Windows, C:\\Program Files etc. are READ-ONLY.\n"
" D:\\ E:\\ and other drives are fully writable.\n\n"
"Virtual FS: /proc /sys /dev /run are shown but not listed\n"
" (prevents system hang).",
parent=self.master,
)
def _ctx_props(self):
path = self._sel_path()
if not path:
return
try:
st = os.stat(path)
mod = datetime.datetime.fromtimestamp(st.st_mtime).strftime(
"%Y-%m-%d %H:%M:%S")
protected = "Yes (read-only)" if _is_protected(path) else "No"
name = os.path.basename(path)
# Build rows as (label, value) pairs
rows = [
("Name", name),
("Path", path),
("Size", f"{st.st_size:,} bytes"),
("Modified", mod),
("Protected", protected),
]
# ---- Custom auto-sizing Toplevel ----
dlg = tk.Toplevel(self.master)
dlg.title("Properties")
dlg.resizable(False, False)
dlg.grab_set() # modal
frm = tk.Frame(dlg, padx=18, pady=14)
frm.pack(fill=tk.BOTH, expand=True)
# column 1 stretches so the window grows to fit the longest value
frm.columnconfigure(1, weight=1)
FONT_BOLD = ("Courier", 10, "bold")
FONT_PLAIN = ("Courier", 10)
for r, (lbl, val) in enumerate(rows):
tk.Label(frm, text=f"{lbl} :", anchor="w",
font=FONT_BOLD).grid(
row=r, column=0, sticky="w", padx=(0, 10), pady=2)
# wraplength=0 → no text wrapping; window auto-widens instead
tk.Label(frm, text=val, anchor="w",
font=FONT_PLAIN, wraplength=0).grid(
row=r, column=1, sticky="w", pady=2)
tk.Button(dlg, text="OK", width=10,
command=dlg.destroy,
default=tk.ACTIVE).pack(pady=(0, 10))
dlg.bind("<Return>", lambda _e: dlg.destroy())
dlg.bind("<Escape>", lambda _e: dlg.destroy())
# Let Tk compute natural size FIRST, then center over parent
dlg.update_idletasks()
dw = dlg.winfo_reqwidth()
dh = dlg.winfo_reqheight()
mx = self.master.winfo_rootx() + (self.master.winfo_width() - dw) // 2
my = self.master.winfo_rooty() + (self.master.winfo_height() - dh) // 2
dlg.geometry(f"{dw}x{dh}+{mx}+{my}")
dlg.wait_window()
except OSError as exc:
messagebox.showerror("Properties Error", str(exc), parent=self.master)
# ------------------------------------------------------------------
# MISC
# ------------------------------------------------------------------
def _set_status(self, text: str):
self.lbl_status.config(text=text)
def _on_quit(self):
if messagebox.askyesno("Quit", "Are you sure you want to quit?",
icon=messagebox.QUESTION, parent=self.master):
self.master.destroy()
def _center_window(self):
sw = self.master.winfo_screenwidth()
sh = self.master.winfo_screenheight()
self.master.geometry(f"900x600+{(sw-900)//2}+{(sh-600)//2}")
# ---------------------------------------------------------------------------
def main():
root = tk.Tk()
FileExplorerApp(root)
root.mainloop()
input("\nPress ENTER to exit.")
if __name__ == "__main__":
main()

