Skip to content
File and folder dialogs

File and folder dialogs

The file dialogs hand your callback a reader, a writer or a folder, not a file path. That is what lets the same code work with a desktop file system, with the document pickers on Android and iOS, and in a browser. Read Files and URIs for how to use what you get back.

Opening a file

dialog.NewFileOpen gives your callback a bellui.URIReadCloser: an io.ReadCloser that also reports the chosen file’s URI.

open := widget.NewButton("Open…", func() {
	d := dialog.NewFileOpen(func(r bellui.URIReadCloser, err error) {
		if err != nil {
			dialog.ShowError(err, w)
			return
		}
		if r == nil {
			return // the user cancelled
		}
		defer r.Close()

		data, err := io.ReadAll(r)
		if err != nil {
			dialog.ShowError(err, w)
			return
		}
		text.SetText(string(data))
		status.SetText("Opened " + r.URI().Name())
	}, w)
	d.SetFilter(storage.NewExtensionFileFilter([]string{".txt", ".md"}))
	d.Show()
})

Three rules for the callback:

  • If the user cancels, the reader is nil and so is the error. Check for both.
  • If the reader is not nil, you must close it.
  • The callback runs on the UI thread, so it can update widgets directly.

SetFilter limits which files can be chosen. storage.NewExtensionFileFilter matches extensions such as .txt (with the dot, ignoring case), and storage.NewMimeTypeFileFilter matches MIME types, including wildcards like image/*.

Saving a file

dialog.NewFileSave works the same way but hands you a bellui.URIWriteCloser. If the user picks a file that already exists, they are asked to confirm before it is overwritten. SetFileName suggests a name.

save := widget.NewButton("Save as…", func() {
	d := dialog.NewFileSave(func(wr bellui.URIWriteCloser, err error) {
		if err != nil {
			dialog.ShowError(err, w)
			return
		}
		if wr == nil {
			return // the user cancelled
		}
		defer wr.Close()

		if _, err := wr.Write([]byte(text.Text)); err != nil {
			dialog.ShowError(err, w)
			return
		}
		status.SetText("Saved " + wr.URI().Name())
	}, w)
	d.SetFileName("notes.txt")
	d.Show()
})

Choosing a folder

dialog.ShowFolderOpen returns a bellui.ListableURI, whose List method returns the folder’s contents.

folder := widget.NewButton("Choose folder…", func() {
	dialog.ShowFolderOpen(func(dir bellui.ListableURI, err error) {
		if err != nil || dir == nil {
			return
		}
		items, err := dir.List()
		if err != nil {
			dialog.ShowError(err, w)
			return
		}
		status.SetText(dir.Name() + " holds " + strconv.Itoa(len(items)) + " items")
	}, w)
})

Choosing several files

dialog.ShowFileOpenMultiple lets the user pick more than one file. The callback gets a slice of readers, or nil if the user cancelled. Close every reader in the slice.

several := widget.NewButton("Open several…", func() {
	dialog.ShowFileOpenMultiple(func(rs []bellui.URIReadCloser, err error) {
		if err != nil || rs == nil {
			return
		}
		for _, r := range rs {
			r.Close() // close every reader you are given
		}
		status.SetText("Picked " + strconv.Itoa(len(rs)) + " files")
	}, w)
})

In Bellui’s own dialog, Ctrl-click (Cmd-click on macOS) adds or removes a file and Shift-click selects a range. On a touch screen, a long press on a file starts selection mode, where each tap adds or removes a file.

Native pickers or Bellui’s dialog

By default the file dialogs use the platform’s own picker where there is one:

PlatformPicker
macOSThe system open and save panels
WindowsThe system file dialogs
Linux and BSDThe desktop’s file chooser, through the XDG desktop portal
Android, iOSThe system document pickers
WebThe browser’s file picker for opening; saving downloads the file

When no native picker is available, for example on a Linux desktop with no portal running, Bellui draws its own dialog instead, so the feature always works. To use Bellui’s dialog everywhere, call dialog.UseNativeFileDialogs(false) once at start-up.

On Android and iOS only the system picker can reach files outside your app’s own storage. With native pickers turned off, Bellui’s dialog on a phone can browse only the app’s private files. If you turn them off for a consistent desktop look, do it only on desktop platforms.

Some details differ between the two:

  • SetShowHidden(true), which shows hidden files, only affects Bellui’s own dialog; native pickers have no such option.
  • A Flatpak build always uses the portal picker, whatever you set.
  • In the browser, opening always uses the browser’s picker and saving is always a download, because a web page has no file system to browse. Folder picking and multi-file picking fall back to Bellui’s dialog there.
  • In the browser the save callback receives a writer at once. The download is offered when you close it, and a callback set with SetOnClosed runs at that moment.

More options

The FileDialog type also has:

  • SetLocation(bellui.ListableURI) to choose the starting folder.
  • SetConfirmText, SetDismissText and SetTitleText to relabel it.
  • SetView(dialog.ListView) or SetView(dialog.GridView) for Bellui’s dialog layout.
  • SetWindowed and SetWindowHosted, as described in Messages and confirmation.

The complete example

// File open, file save and folder dialogs.
package main

import (
	"io"
	"strconv"

	"github.com/belluikit/bellui"
	"github.com/belluikit/bellui/app"
	"github.com/belluikit/bellui/container"
	"github.com/belluikit/bellui/dialog"
	"github.com/belluikit/bellui/storage"
	"github.com/belluikit/bellui/widget"
)

func main() {
	a := app.New()
	w := a.NewWindow("File dialogs")
	text := widget.NewMultiLineEntry()
	status := widget.NewLabel("")

	open := widget.NewButton("Open…", func() {
		d := dialog.NewFileOpen(func(r bellui.URIReadCloser, err error) {
			if err != nil {
				dialog.ShowError(err, w)
				return
			}
			if r == nil {
				return // the user cancelled
			}
			defer r.Close()

			data, err := io.ReadAll(r)
			if err != nil {
				dialog.ShowError(err, w)
				return
			}
			text.SetText(string(data))
			status.SetText("Opened " + r.URI().Name())
		}, w)
		d.SetFilter(storage.NewExtensionFileFilter([]string{".txt", ".md"}))
		d.Show()
	})

	save := widget.NewButton("Save as…", func() {
		d := dialog.NewFileSave(func(wr bellui.URIWriteCloser, err error) {
			if err != nil {
				dialog.ShowError(err, w)
				return
			}
			if wr == nil {
				return // the user cancelled
			}
			defer wr.Close()

			if _, err := wr.Write([]byte(text.Text)); err != nil {
				dialog.ShowError(err, w)
				return
			}
			status.SetText("Saved " + wr.URI().Name())
		}, w)
		d.SetFileName("notes.txt")
		d.Show()
	})

	folder := widget.NewButton("Choose folder…", func() {
		dialog.ShowFolderOpen(func(dir bellui.ListableURI, err error) {
			if err != nil || dir == nil {
				return
			}
			items, err := dir.List()
			if err != nil {
				dialog.ShowError(err, w)
				return
			}
			status.SetText(dir.Name() + " holds " + strconv.Itoa(len(items)) + " items")
		}, w)
	})

	several := widget.NewButton("Open several…", func() {
		dialog.ShowFileOpenMultiple(func(rs []bellui.URIReadCloser, err error) {
			if err != nil || rs == nil {
				return
			}
			for _, r := range rs {
				r.Close() // close every reader you are given
			}
			status.SetText("Picked " + strconv.Itoa(len(rs)) + " files")
		}, w)
	})

	buttons := container.NewHBox(open, save, folder, several)
	w.SetContent(container.NewBorder(buttons, status, nil, nil, text))
	bellui.ShowAndRun(w)
}