File Explorer Tree Generator for local files

Hey all, I needed a reactive file explorer that generated a file tree given a path, so here it is. Big thanks to @AnnMarieW for help building this out!

Screen Recording 2022-10-08 at 5.09.47 PM

Here is the code:

import os
from dash_iconify import DashIconify
import dash_mantine_components as dmc


class FileTree:

    def __init__(self, filepath: os.PathLike):
        """
        Usage: component = FileTree('Path/to/my/File').render()
        """
        self.filepath = filepath

    def render(self) -> dmc.Accordion:
        return dmc.Accordion(
            self.build_tree(self.filepath, isRoot=True),
            multiple=True)

    def flatten(self, l):
        return [item for sublist in l for item in sublist]

    def make_file(self, file_name):
        return dmc.Text(
            [DashIconify(icon="akar-icons:file"), " ", file_name], style={"paddingTop": '5px'}
        )

    def make_folder(self, folder_name):
        return [DashIconify(icon="akar-icons:folder"), " ", folder_name]

    def build_tree(self, path, isRoot=False):
        d = []
        if os.path.isdir(path):
            children = self.flatten([self.build_tree(os.path.join(path, x))
                                    for x in os.listdir(path)])
            if isRoot:
                d.append(
                    dmc.AccordionItem(
                        children=children,
                        label=self.make_folder(os.path.basename(path)))
                )
            else:
                d.append(
                    dmc.Accordion(children=[
                        dmc.AccordionItem(
                            children=children,
                            label=self.make_folder(os.path.basename(path)))
                    ],
                        multiple=True)
                )
        else:
            d.append(self.make_file(os.path.basename(path)))
        return d

# FileTree.render() returns a dash mantine components Accordion

app.layout = FileTree('Path/to/my/Directory').render()
10 Likes

Wow, this is good. One suggestion: we can move the arrow icon to the right, would that look better?
Can’t wait to try this out @andrew-hossack.

I personally like the arrow on the left; to me it makes more sense to have the arrow icon be the first part of the dropdown. Would you be able to change Accordion to show what it would look like?

I’m also curious to know if there is a way to set Accordion to expanded when it is created. State seems like it should control open/close, but setting state as a kwarg doesn’t work (it needs a dict). How do I achieve this functionality?

Hi, the code did not work for me because dash_mantine_components changed a bit.

I made minimal changes to keep it working + an example on how to select the folder:

app/
├─ server.py
├─ local.py

server.py

from dash import Dash, html


import os
from dash_iconify import DashIconify
import dash_mantine_components as dmc

class FileTree:
    def __init__(self, filepath: os.PathLike,id:str):
        """
        Usage: component = FileTree('Path/to/my/File').render()
        """
        self.id = id
        self.filepath = filepath

    def render(self) -> dmc.Accordion:
        return dmc.AccordionMultiple(FileTree.build_tree(self.filepath, isRoot=True),id=self.id)

    @staticmethod
    def flatten(l):
        return [item for sublist in l for item in sublist]

    @staticmethod
    def make_file(file_name):
        return dmc.Text(
            [DashIconify(icon="akar-icons:file"), " ", file_name],
            style={"paddingTop": "5px"},
        )

    @staticmethod
    def make_folder(folder_name):
        return [DashIconify(icon="akar-icons:folder"), " ", folder_name]

    @staticmethod
    def build_tree(path, isRoot=False):
        d = []
        if os.path.isdir(path): # if it is a folder
            children = [FileTree.build_tree(os.path.join(path, x)) for x in os.listdir(path)]
            print(children)
            if isRoot:
                return FileTree.flatten(children)
            item = dmc.AccordionItem(
                [
                    dmc.AccordionControl(FileTree.make_folder(os.path.basename(path))),
                    dmc.AccordionPanel(children=FileTree.flatten(children))
                ],value=path)
            d.append(item)
            

        else:
            d.append(FileTree.make_file(os.path.basename(path)))
        return d

app = Dash(__name__)

import dash_bootstrap_components as dbc

tree = FileTree(".","file_tree").render()


app.layout = html.Div(
    [
        html.H1("Hello Dash"),
        dbc.Button("Select folder", id="select_folder", className="mr-1"),
        tree
        
    ]
)
from dash.dependencies import Input, Output

import subprocess


@app.callback(
    Output("file_tree", "children"),
    [Input("select_folder", "n_clicks")],
)
def add(n_clicks):
    if n_clicks > 0:
        command = ['python', './local.py']
        p = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        out, err = p.communicate()
        print(out,err)
        path = out.decode("utf-8").strip()
        children = FileTree.build_tree(path, isRoot=True)
        print(children)
        return children

if __name__ == "__main__":
    app.run_server(debug=True)

local.py


import tkinter as tk
from tkinter import filedialog
root = tk.Tk()
root.withdraw()
root.attributes('-topmost',True)
folder_selected = filedialog.askdirectory()
if folder_selected:
    print(folder_selected)
else:
    print(None)

Thank you for posting this snippet!

1 Like

Try using dmc.NavLink.

2 Likes

I made some updates as well if anyone wants to use this:

class FileTree:

    def __init__(self, filepath: os.PathLike):
        """
        Usage: component = FileTree('Path/to/my/File').render()
        """
        self.filepath = filepath

    def render(self) -> dmc.AccordionMultiple:
        return dmc.AccordionMultiple(
            self.build_tree(self.filepath, isRoot=True))

    def flatten(self, l):
        return [item for sublist in l for item in sublist]

    def make_file(self, file_name):
        return dmc.Text(
            [DashIconify(icon="akar-icons:file"), " ", file_name], style={"paddingTop": '5px'}
        )

    def make_folder(self, folder_name):
        return [DashIconify(icon="akar-icons:folder"), " ", folder_name]

    def build_tree(self, path, isRoot=False):
        d = []
        if os.path.isdir(path):
            children = self.flatten([self.build_tree(os.path.join(path, x))
                                    for x in os.listdir(path)])
            if isRoot:
                d.append(
                    dmc.AccordionItem([
                        dmc.AccordionControl(self.make_folder(os.path.basename(path))),
                        dmc.AccordionPanel(children=children)
                        ], value=str(path))
                )
            else:
                d.append(
                    dmc.AccordionMultiple(children=[
                        dmc.AccordionItem([
                            dmc.AccordionControl(self.make_folder(os.path.basename(path))),
                            dmc.AccordionPanel(children=children)
                            ], value=str(path))
                    ])
                )
        else:
            d.append(self.make_file(os.path.basename(path)))
        return d