Skip to content

Using the library

The dialogs

textual-fspicker provides three public dialogs: one for selecting a file for opening, one for selecting a file for saving, and one for selecting a pre-existing directory. Each dialog is implemented as a Textual modal screen.

Each of the dialogs is designed to return a dismiss result of the Path of the filesystem entry that was selected, or None if the user cancelled the dialog.

The usual pattern for using one of the dialogs, using Textual's ability to wait for a screen will look something like this:

from textual_fspicker import FileSave

...

class SomeApp(App):

   ...

   @on(Button.Clicked, "#save")
   @work
   async def save_document(self) -> None:
       """Save the document."""
       if save_to := await self.push_screen_wait(FileSave()):
           my_saving_function(save_to)
           self.notify("Saved")
       else:
           self.notify("Save cancelled")

Opening a file

The FileOpen dialog is used to prompt the user for file to open. The most basic example looks like this:

from textual import on, work
from textual.app import App, ComposeResult
from textual.widgets import Button, Label

from textual_fspicker import FileOpen


class BasicFileOpenApp(App[None]):
    def compose(self) -> ComposeResult:
        yield Button("Press to open a file")
        yield Label()

    @on(Button.Pressed)
    @work
    async def open_a_file(self) -> None:
        if opened := await self.push_screen_wait(FileOpen()):
            self.query_one(Label).update(str(opened))


if __name__ == "__main__":
    BasicFileOpenApp().run()

BasicFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

BasicFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁ Open ─────────────────────────────────────────────────────── 📁 ..                         1280 2025-02-08 07:40:38 📁 dist                        128 2025-02-19 23:06:05 📁 docs                        128 2025-02-27 21:21:27 📁 img                          96 2023-05-18 20:57:17 📁 src                          96 2025-01-15 22:22:32 📄 ChangeLog.md               4127 2025-02-20 21:33:59 📄 LICENSE                    1087 2023-05-14 18:22:29 📄 Makefile                   3577 2025-02-27 21:21:27▂▂ 📄 README.md                  1810 2025-01-15 22:22:32 📄 mkdocs.yml                 2044 2025-02-27 21:21:27 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ OpenCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────────────────────────────

BasicFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ /Users/davep/develop/python/textual-fspicker/ChangeLog.md

Setting the default file

When opening a file, you may want to specify a default filename which will be shown to the user when the dialog opens; this can be done with the default_file keyword parameter:

from textual import on, work
from textual.app import App, ComposeResult
from textual.widgets import Button, Label

from textual_fspicker import FileOpen


class DefaultFileOpenApp(App[None]):
    def compose(self) -> ComposeResult:
        yield Button("Press to open a file")
        yield Label()

    @on(Button.Pressed)
    @work
    async def open_a_file(self) -> None:
        if opened := await self.push_screen_wait(FileOpen(default_file="README.md")):
            self.query_one(Label).update(str(opened))


if __name__ == "__main__":
    DefaultFileOpenApp().run()

DefaultFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

DefaultFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁ Open ─────────────────────────────────────────────────────── 📁 ..                         1280 2025-02-08 07:40:38 📁 dist                        128 2025-02-19 23:06:05 📁 docs                        128 2025-02-27 21:21:27 📁 img                          96 2023-05-18 20:57:17 📁 src                          96 2025-01-15 22:22:32 📄 ChangeLog.md               4127 2025-02-20 21:33:59 📄 LICENSE                    1087 2023-05-14 18:22:29 📄 Makefile                   3577 2025-02-27 21:21:27▂▂ 📄 README.md                  1810 2025-01-15 22:22:32 📄 mkdocs.yml                 2044 2025-02-27 21:21:27 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ README.mdOpenCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────────────────────────────

DefaultFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ /Users/davep/develop/python/textual-fspicker/README.md

Ensuring the file exists

A user can select a file by either picking one from the directory navigation widget within the dialog, or by typing the path and name of a file in the Input widget in the dialog. If they type in a name it's possible for them to type in the name of a file that doesn't exist.

In such a case the dialog will refuse to close and an error will be shown:

BasicFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁ Open ─────────────────────────────────────────────────────── 📁 ..                         1280 2025-02-08 07:40:38 📁 dist                        128 2025-02-19 23:06:05 📁 docs                        128 2025-02-27 21:21:27 📁 img                          96 2023-05-18 20:57:17 📁 src                          96 2025-01-15 22:22:32 📄 ChangeLog.md               4127 2025-02-20 21:33:59 📄 LICENSE                    1087 2023-05-14 18:22:29 📄 Makefile                   3577 2025-02-27 21:21:27▂▂ 📄 README.md                  1810 2025-01-15 22:22:32 📄 mkdocs.yml                 2044 2025-02-27 21:21:27 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ bad.pyOpenCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────── The file must exist 

If you want the allow the user to "open" a file that doesn't really exist, in other words you want them to be able to type in any name they wish, you can set the must_exist keyword parameter to False:

from textual import on, work
from textual.app import App, ComposeResult
from textual.widgets import Button, Label

from textual_fspicker import FileOpen


class DefaultFileOpenApp(App[None]):
    def compose(self) -> ComposeResult:
        yield Button("Press to open a file")
        yield Label()

    @on(Button.Pressed)
    @work
    async def open_a_file(self) -> None:
        if opened := await self.push_screen_wait(FileOpen(must_exist=False)):
            self.query_one(Label).update(str(opened))


if __name__ == "__main__":
    DefaultFileOpenApp().run()

DefaultFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

DefaultFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁ Open ─────────────────────────────────────────────────────── 📁 ..                         1280 2025-02-08 07:40:38 📁 dist                        128 2025-02-19 23:06:05 📁 docs                        128 2025-02-27 21:21:27 📁 img                          96 2023-05-18 20:57:17 📁 src                          96 2025-01-15 22:22:32 📄 ChangeLog.md               4127 2025-02-20 21:33:59 📄 LICENSE                    1087 2023-05-14 18:22:29 📄 Makefile                   3577 2025-02-27 21:21:27▂▂ 📄 README.md                  1810 2025-01-15 22:22:32 📄 mkdocs.yml                 2044 2025-02-27 21:21:27 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ OpenCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────────────────────────────

DefaultFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁ Open ─────────────────────────────────────────────────────── 📁 ..                         1280 2025-02-08 07:40:38 📁 dist                        128 2025-02-19 23:06:05 📁 docs                        128 2025-02-27 21:21:27 📁 img                          96 2023-05-18 20:57:17 📁 src                          96 2025-01-15 22:22:32 📄 ChangeLog.md               4127 2025-02-20 21:33:59 📄 LICENSE                    1087 2023-05-14 18:22:29 📄 Makefile                   3577 2025-02-27 21:21:27▂▂ 📄 README.md                  1810 2025-01-15 22:22:32 📄 mkdocs.yml                 2044 2025-02-27 21:21:27 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ bad.pyOpenCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────────────────────────────

DefaultFileOpenApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to open a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ /Users/davep/develop/python/textual-fspicker/bad.py

Saving a file

The FileSave dialog is used to prompt the user for file to save. The most basic example looks like this:

from textual import on, work
from textual.app import App, ComposeResult
from textual.widgets import Button, Label

from textual_fspicker import FileSave


class BasicFileSaveApp(App[None]):
    def compose(self) -> ComposeResult:
        yield Button("Press to save a file")
        yield Label()

    @on(Button.Pressed)
    @work
    async def save_a_file(self) -> None:
        if opened := await self.push_screen_wait(FileSave()):
            self.query_one(Label).update(str(opened))


if __name__ == "__main__":
    BasicFileSaveApp().run()

BasicFileSaveApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to save a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

BasicFileSaveApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to save a file ▁▁▁▁▁▁▁▁ Save as ──────────────────────────────────────────────────── 📁 ..                         1280 2025-02-08 07:40:38 📁 dist                        128 2025-02-19 23:06:05 📁 docs                        128 2025-02-27 21:21:27 📁 img                          96 2023-05-18 20:57:17 📁 src                          96 2025-01-15 22:22:32 📄 ChangeLog.md               4127 2025-02-20 21:33:59 📄 LICENSE                    1087 2023-05-14 18:22:29 📄 Makefile                   3577 2025-02-27 21:21:27▂▂ 📄 README.md                  1810 2025-01-15 22:22:32 📄 mkdocs.yml                 2044 2025-02-27 21:21:27 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ SaveCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────────────────────────────

BasicFileSaveApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to save a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ /Users/davep/develop/python/textual-fspicker/ChangeLog.md

Setting the default file

When prompting for a file to save to, you may want to specify a default filename which will be shown to the user when the dialog opens; this can be done with the default_file keyword parameter:

from textual import on, work
from textual.app import App, ComposeResult
from textual.widgets import Button, Label

from textual_fspicker import FileSave


class DefaultSaveFileApp(App[None]):
    def compose(self) -> ComposeResult:
        yield Button("Press to save a file")
        yield Label()

    @on(Button.Pressed)
    @work
    async def save_a_file(self) -> None:
        if opened := await self.push_screen_wait(FileSave(default_file="example.md")):
            self.query_one(Label).update(str(opened))


if __name__ == "__main__":
    DefaultSaveFileApp().run()

DefaultSaveFileApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to save a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

DefaultSaveFileApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to save a file ▁▁▁▁▁▁▁▁ Save as ──────────────────────────────────────────────────── 📁 ..                         1280 2025-02-08 07:40:38 📁 dist                        128 2025-02-19 23:06:05 📁 docs                        128 2025-02-27 21:21:27 📁 img                          96 2023-05-18 20:57:17 📁 src                          96 2025-01-15 22:22:32 📄 ChangeLog.md               4127 2025-02-20 21:33:59 📄 LICENSE                    1087 2023-05-14 18:22:29 📄 Makefile                   3577 2025-02-27 21:21:27▂▂ 📄 README.md                  1810 2025-01-15 22:22:32 📄 mkdocs.yml                 2044 2025-02-27 21:21:27 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ example.mdSaveCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────────────────────────────

DefaultSaveFileApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to save a file ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ /Users/davep/develop/python/textual-fspicker/example.md

Preventing overwrite of an existing file

When it comes to picking a file to save to, the user can either select a pre-existing file, or they can enter the name of a new file. Sometimes you may want to use this dialog to prompt them for a file to save to, but you want to prevent them from overwriting an existing file. This can be done with the can_overwrite parameter. If set to False the dialog will refuse to close while an existing file is selected:

from textual import on, work
from textual.app import App, ComposeResult
from textual.widgets import Button, Label

from textual_fspicker import FileSave


class DefaultSaveFileApp(App[None]):
    def compose(self) -> ComposeResult:
        yield Button("Press to save a file")
        yield Label()

    @on(Button.Pressed)
    @work
    async def save_a_file(self) -> None:
        if opened := await self.push_screen_wait(FileSave(can_overwrite=False)):
            self.query_one(Label).update(str(opened))


if __name__ == "__main__":
    DefaultSaveFileApp().run()

DefaultSaveFileApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to save a file ▁▁▁▁▁▁▁▁ Save as ──────────────────────────────────────────────────── 📁 ..                         1280 2025-02-08 07:40:38 📁 dist                        128 2025-02-19 23:06:05 📁 docs                        128 2025-02-27 21:21:27 📁 img                          96 2023-05-18 20:57:17 📁 src                          96 2025-01-15 22:22:32 📄 ChangeLog.md               4127 2025-02-20 21:33:59 📄 LICENSE                    1087 2023-05-14 18:22:29 📄 Makefile                   3577 2025-02-27 21:21:27▂▂ 📄 README.md                  1810 2025-01-15 22:22:32 📄 mkdocs.yml                 2044 2025-02-27 21:21:27 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ ChangeLog.mdSaveCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ─────────────────────────────────── Overwrite is not allowed 

Picking a directory

The SelectDirectory dialog is used to prompt the user for a directory. The most basic example looks like this:

from textual import on, work
from textual.app import App, ComposeResult
from textual.widgets import Button, Label

from textual_fspicker import SelectDirectory


class BasicSelectDirectoryApp(App[None]):
    def compose(self) -> ComposeResult:
        yield Button("Press to select a directory")
        yield Label()

    @on(Button.Pressed)
    @work
    async def pick_a_directory(self) -> None:
        if opened := await self.push_screen_wait(SelectDirectory()):
            self.query_one(Label).update(str(opened))


if __name__ == "__main__":
    BasicSelectDirectoryApp().run()

BasicSelectDirectoryApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to select a directory ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

BasicSelectDirectoryApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to select a directory ▁▁▁▁▁▁▁▁ Select directory ─────────────────────────────────────────── 📁 ..                           1280 2025-02-08 07:40:38 📁 docs                          128 2025-02-27 21:21:27 📁 img                            96 2023-05-18 20:57:17 📁 src                            96 2025-01-15 22:22:32 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ …ython/textual-fspickerSelectCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ──────────────────────────────────────────────────────────────

BasicSelectDirectoryApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Press to select a directory ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ /Users/davep/develop/python/textual-fspicker/img

Filtering

The FileOpen and FileSave dialogs have an optional filter facility; this displays as a Textual Select widget within the dialog and provides the user with a list of prompts that can filter down the content of the dialog.

For example:

BasicFileOpenApp  Open ─────────────────────────────────────────────────────────────────────── 📁 ..                                           1280 2025-02-08 07:40:38 📁 docs                                          128 2025-02-27 21:21:27 📁 img                                            96 2023-05-18 20:57:17 📁 src                                            96 2025-01-15 22:22:32 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ PythonOpenCancel ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▔▔▔▔▔▔▔▔▔▔▔▔▔ ────────────────────────────Python─────────────────────────────────── Markdown TOML YAML All ▁▁▁▁▁▁▁▁▁▁▁▁▁

The filters are passed to either dialog using the filters keyword argument, the value being a Filters object. Filters takes as its parameters a series of tuples, each comprising of a string label and a function that takes a Path and returns a bool. For any given function, if it returns True the file will be included in the display, if False it will be filtered out.

The code for the dialog shown above would look something like this:

FileOpen(
    filters=Filters(
        ("Python", lambda p: p.suffix.lower() == ".py"),
        ("Markdown", lambda p: p.suffix.lower() == ".md"),
        ("TOML", lambda p: p.suffix.lower() == ".toml"),
        ("YAML", lambda p: p.suffix.lower() == ".yaml"),
        ("All", lambda _: True),
    )
)