#!/usr/bin/env python3
# ============================================================
# Slick Browser 2.x
# Author: R. Seaverns (K0NxT3D)
# URL: http://www.seaverns.com/
# ------------------------------------------------------------
# Directory Tree HTML Export Utility
#
# Description:
#   Lightweight desktop application for generating a
#   clickable HTML directory tree. Designed for quick
#   filesystem mapping, project structure analysis,
#   and day-to-day directory forensics.
#
# Primary Use Cases:
#   - Incident response / forensic triage
#   - Repository structure documentation
#   - Template validation / project audits
#   - Archive verification
#   - Educational filesystem demonstrations
#
# Features:
#   - Depth-controlled recursion
#   - Exclusion filter (glob patterns)
#   - Item cap safety limit
#   - Standalone HTML report output
#   - Zero third-party dependencies
#
# Requirements:
#   Python 3.9+
#   Tkinter (included with most Python distributions)
#
# License:
#   Internal / Private Use (adjust as needed)
#
# Author:
#   Your Name / Organization
#
# Build:
#   Standalone binary recommended for release builds.
#
# ============================================================


import os
import sys
import fnmatch
import html
import time
import threading
import subprocess
from pathlib import Path
import tkinter as tk
from tkinter import ttk, filedialog, messagebox

APP_TITLE = "Slick Browser 2.1"


# ----------------------------
# Helpers
# ----------------------------

def open_with_system(path):
    try:
        if sys.platform.startswith("win"):
            os.startfile(path)
        elif sys.platform == "darwin":
            subprocess.Popen(["open", path])
        else:
            subprocess.Popen(["xdg-open", path])
    except Exception:
        return False
    return True


def normalize_patterns(raw):
    if not raw.strip():
        return []
    parts = []
    for chunk in raw.replace("|", ",").split(","):
        s = chunk.strip()
        if s:
            parts.append(s)
    return parts


def should_exclude(rel, name, patterns):
    for pat in patterns:
        p = pat.replace("\\", "/")
        if fnmatch.fnmatch(name, p) or fnmatch.fnmatch(rel, p):
            return True
    return False


def safe_scandir(path):
    try:
        with os.scandir(path) as it:
            for entry in it:
                yield entry
    except Exception:
        return


def to_file_url(p):
    try:
        return p.resolve().as_uri()
    except Exception:
        return "file://" + str(p.resolve())


# ----------------------------
# HTML
# ----------------------------

HTML_TEMPLATE = """<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{title}</title>
<style>
body {{
  margin: 0;
  background: #0c0c0c;
  color: #ffffff;
  font-family: monospace;
}}

header {{
  padding: 14px;
  background: #111;
  border-bottom: 1px solid #1a1a1a;
}}

.tree {{
  padding: 14px;
}}

ul {{
  list-style: none;
  padding-left: 18px;
  border-left: 1px solid #1a1a1a;
}}

a {{
  color: #990000;
  text-decoration: none;
}}

a:hover {{
  text-decoration: underline;
}}

details > summary {{
  cursor: pointer;
  user-select: none;
}}

details > summary::before {{
  content: '+ ';
}}

details[open] > summary::before {{
  content: '− ';
}}
</style>
</head>
<body>
<header>
<b>{title}</b><br>
Root: {root}<br>
Depth: {depth}<br>
Items: {items}
</header>
<div class="tree">
{body}
</div>
</body>
</html>
"""


def build_tree(root_dir, depth_limit, exclude_raw, item_cap):
    patterns = normalize_patterns(exclude_raw)
    stats = {"items": 0}

    def rel(p):
        try:
            return p.relative_to(root_dir).as_posix()
        except Exception:
            return p.as_posix()

    def walk(path, depth):
        if depth > depth_limit:
            return ""
        if stats["items"] >= item_cap:
            return ""

        out = ["<ul>"]
        dirs = []
        files = []

        for entry in safe_scandir(path):
            child = Path(entry.path)
            r = rel(child)

            if should_exclude(r, child.name, patterns):
                continue

            try:
                if entry.is_dir(follow_symlinks=False):
                    dirs.append(child)
                else:
                    files.append(child)
            except Exception:
                continue

        dirs.sort(key=lambda p: p.name.lower())
        files.sort(key=lambda p: p.name.lower())

        for d in dirs:
            if stats["items"] >= item_cap:
                break
            stats["items"] += 1
            out.append(
                "<li><details>"
                f"<summary>{html.escape(d.name)}</summary>"
                f"{walk(d, depth + 1)}"
                "</details></li>"
            )

        for f in files:
            if stats["items"] >= item_cap:
                break
            stats["items"] += 1
            url = html.escape(to_file_url(f))
            out.append(
                "<li>"
                f"<a href='{url}'>{html.escape(f.name)}</a>"
                "</li>"
            )

        out.append("</ul>")
        return "\n".join(out)

    body = walk(root_dir, 0)

    return HTML_TEMPLATE.format(
        title=APP_TITLE,
        root=str(root_dir),
        depth=depth_limit,
        items=stats["items"],
        body=body,
    )


# ----------------------------
# GUI
# ----------------------------

class SlickExporter(tk.Tk):

    def __init__(self):
        super().__init__()
        self.title(APP_TITLE)
        self.geometry("650x180")
        self.resizable(False, False)
        self.configure(bg="#0c0c0c")

        self.path_var = tk.StringVar(value=str(Path.home()))
        self.depth_var = tk.IntVar(value=3)
        self.exclude_var = tk.StringVar(value=".git, node_modules")
        self.itemcap_var = tk.IntVar(value=40000)

        self.status_var = tk.StringVar(value="Ready.")

        self.build_ui()

    def build_ui(self):
        style = ttk.Style(self)
        style.theme_use("clam")

        style.configure("Dark.TFrame", background="#222222")
        style.configure("Dark.TLabel", background="#222222", foreground="#ffffff")
        style.configure("Dark.TButton", background="#1a1a1a", foreground="#ffffff")
        style.map("Dark.TButton", background=[("active", "#990000")])

        frame = ttk.Frame(self, padding=12, style="Dark.TFrame")
        frame.pack(fill="both", expand=True)

        row1 = ttk.Frame(frame, style="Dark.TFrame")
        row1.pack(fill="x")

        ttk.Label(row1, text="Path:", style="Dark.TLabel").pack(side="left")
        ttk.Entry(row1, textvariable=self.path_var, width=60).pack(side="left", padx=8)
        ttk.Button(row1, text="Browse", style="Dark.TButton",
                   command=self.browse).pack(side="left")

        row2 = ttk.Frame(frame, style="Dark.TFrame")
        row2.pack(fill="x", pady=10)

        ttk.Label(row2, text="Depth:", style="Dark.TLabel").pack(side="left")
        ttk.Spinbox(row2, from_=0, to=99,
                    textvariable=self.depth_var,
                    width=5).pack(side="left", padx=8)

        ttk.Label(row2, text="Item cap:", style="Dark.TLabel").pack(side="left")
        ttk.Spinbox(row2, from_=1000, to=500000,
                    increment=1000,
                    textvariable=self.itemcap_var,
                    width=8).pack(side="left", padx=8)

        ttk.Label(row2, text="Exclude:", style="Dark.TLabel").pack(side="left")
        ttk.Entry(row2, textvariable=self.exclude_var,
                  width=30).pack(side="left", padx=8)

        row3 = ttk.Frame(frame, style="Dark.TFrame")
        row3.pack(fill="x")

        ttk.Button(row3, text="Export",
                   style="Dark.TButton",
                   command=self.export_html).pack(side="left")

        ttk.Button(row3, text="Quit",
                   style="Dark.TButton",
                   command=self.destroy).pack(side="right")

        ttk.Label(frame, textvariable=self.status_var,
                  style="Dark.TLabel").pack(anchor="w", pady=8)

    def browse(self):
        path = filedialog.askdirectory(initialdir=self.path_var.get())
        if path:
            self.path_var.set(path)

    def export_html(self):
        root_dir = Path(self.path_var.get()).expanduser()
        if not root_dir.exists():
            messagebox.showerror(APP_TITLE, "Invalid directory")
            return

        save_path = filedialog.asksaveasfilename(
            defaultextension=".html",
            filetypes=[("HTML files", "*.html")]
        )
        if not save_path:
            return

        self.status_var.set("Generating...")
        self.update_idletasks()

        html_doc = build_tree(
            root_dir,
            self.depth_var.get(),
            self.exclude_var.get(),
            self.itemcap_var.get()
        )

        with open(save_path, "w", encoding="utf-8") as f:
            f.write(html_doc)

        self.status_var.set("Export complete.")
        open_with_system(save_path)


if __name__ == "__main__":
    app = SlickExporter()
    app.mainloop()

