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()
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()
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:
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()
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()
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()
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()
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()
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:
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),
)
)