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
1 Like

I made some further improvements to the FileTree.
Folders in FileTree are now accompanied by a checkbox. If a checkbox is selected, that folder is used as the root of a new FileTree.
The fictituous folder ‘…’ is added on top of the folder, so that the user can traverse back to the initial root. I limited this so that the initial root can not be escaped by the user.

For clarity and ease of use, I put all in a working example in a single file:

from dash import Dash, Input, Output, html, ALL, no_update, callback_context
import dash_mantine_components as dmc
from dash_iconify import DashIconify
from pathlib import Path


class FileTree:
    def __init__(self, filepath: Path):
        self.filepath = Path(filepath)

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

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

    def make_file(self, file_name):
        return dmc.Group([
            # Spacing element to align the checkbox with the file icon
            dmc.Space(w=35),
            DashIconify(icon="akar-icons:file"),
            dmc.Text(file_name)
        ], style={"paddingTop": '5px'})

    def make_folder(self, folder_name, path):
        return dmc.Group([
            dmc.Checkbox(id={'type': 'folder_checkbox', 'index': str(path)}),
            DashIconify(icon="akar-icons:folder"),
            dmc.Text(folder_name)
        ])

    def build_tree(self, path, is_root=False):
        d = []
        path = Path(path)
        if path.is_dir():
            children = self.flatten([self.build_tree(child)
                                     for child in path.iterdir()])
            if is_root and path != INITIAL_FOLDER:
                d.append(
                    dmc.AccordionItem([
                        dmc.AccordionControl(
                            self.make_folder('..', path.parent)),
                    ], value='..')
                )
            d.append(
                dmc.AccordionItem([
                    dmc.AccordionControl(
                        self.make_folder(path.name, path)),
                    dmc.AccordionPanel(children=children)
                ], value=str(path))
            )
        else:
            d.append(self.make_file(path.name))
        return d


INITIAL_FOLDER = Path(r'C:\data\experimental')
app = Dash(__name__)


# Define the callback
@app.callback(
    Output('filetree_div', 'children'),
    Output('selected_folder_title', 'children'),
    Input({'type': 'folder_checkbox', 'index': ALL}, 'checked')
)
def update_output(checked_values):
    '''
    Update the file tree and selected folder title based on the checked boxes
    '''
    if checked_values is None:
        return 'No paths selected'

    # Extract the paths of the checked checkboxes
    checked_paths = [item['id']['index'] for item, checked in zip(
        callback_context.inputs_list[0], checked_values) if checked]

    if checked_paths:
        # Render a new FileTree with the selected folder as the root
        return FileTree(checked_paths[0]).render(), checked_paths[0]
    else:
        return no_update


# Add an output div to your layout
app.layout = html.Div([
    html.H2(id='selected_folder_title', children=str(INITIAL_FOLDER)),
    html.Div(id='filetree_div',
             children=FileTree(INITIAL_FOLDER).render())
])

if __name__ == '__main__':
    app.run_server(debug=True)
1 Like

Welcome to the forums and thanks for sharing this! @luuk

I adapted it slightly to make it compatible with dmc 0.15.1:

from dash import Dash, Input, Output, html, ALL, no_update, callback_context, _dash_renderer
import dash_mantine_components as dmc
from dash_iconify import DashIconify
from pathlib import Path

_dash_renderer._set_react_version("18.2.0")


class FileTree:
    def __init__(self, filepath: Path):
        self.filepath = Path(filepath)

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

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

    def make_file(self, file_name):
        return dmc.Group([
            # Spacing element to align the checkbox with the file icon
            dmc.Space(w=35),
            DashIconify(icon="akar-icons:file"),
            dmc.Text(file_name)
        ], style={"paddingTop": '5px'})

    def make_folder(self, folder_name, path):
        return dmc.Group([
            dmc.Checkbox(id={'type': 'folder_checkbox', 'index': str(path)}),
            DashIconify(icon="akar-icons:folder"),
            dmc.Text(folder_name)
        ])

    def build_tree(self, path, is_root=False):
        d = []
        path = Path(path)
        if path.is_dir():
            children = self.flatten([self.build_tree(child)
                                     for child in path.iterdir()])
            if is_root and path != INITIAL_FOLDER:
                d.append(
                    dmc.AccordionItem([
                        dmc.AccordionControl(
                            self.make_folder('..', path.parent)),
                    ], value='..')
                )
            d.append(
                dmc.AccordionItem([
                    dmc.AccordionControl(
                        self.make_folder(path.name, path)),
                    dmc.AccordionPanel(children=children)
                ], value=str(path))
            )
        else:
            d.append(self.make_file(path.name))
        return d


INITIAL_FOLDER = Path(r'data/')

app = Dash(__name__)


# Define the callback
@app.callback(
    Output('filetree_div', 'children'),
    Output('selected_folder_title', 'children'),
    Input({'type': 'folder_checkbox', 'index': ALL}, 'checked')
)
def update_output(checked_values):
    '''
    Update the file tree and selected folder title based on the checked boxes
    '''
    if checked_values is None:
        return 'No paths selected'

    # Extract the paths of the checked checkboxes
    checked_paths = [item['id']['index'] for item, checked in zip(
        callback_context.inputs_list[0], checked_values) if checked]

    if checked_paths:
        # Render a new FileTree with the selected folder as the root
        return FileTree(checked_paths[0]).render(), checked_paths[0]
    else:
        return no_update


# Add an output div to your layout
app.layout = dmc.MantineProvider(
    children=[
        html.Div(
            [
                html.H2(id='selected_folder_title', children=str(INITIAL_FOLDER)),
                html.Div(id='filetree_div', children=FileTree(INITIAL_FOLDER).render())
            ]
        )
    ]
)

if __name__ == '__main__':
    app.run(debug=True)
2 Likes