FolderPicker

FolderPicker draws a button that opens a directory dialog on desktop or in Chromium's Wasm executor. It returns nil before a selection. Selecting a directory reruns the page, and that run receives the new folder. Cancelling keeps the previous selection and does not rerun.

API

type FolderObject = tgframe.Folder

type FolderPickerConf struct {
    tgframe.Base // ID
    Write bool
    DefaultDir string
    Disabled bool
}

func FolderPicker(c *tgframe.Container, label string, conf ...*FolderPickerConf) FolderObject

conf is optional, with at most one configuration. Write requests write access; desktop grants it unconditionally. DefaultDir sets the desktop dialog's initial directory. Disabled disables the button.

The shared interface lives in tgframe:

type Folder interface {
    fs.ReadDirFS // Open and ReadDir
    fs.StatFS
    Create(name string) (io.WriteCloser, error)
    MkdirAll(name string) error
    Writable() bool
    Path() string
}

var ErrFolderReadOnly = tgutil.NewError("folder is read-only")

Names follow fs.ValidPath: slash-separated paths relative to the selected root, with no .. components. Desktop uses os.OpenRoot, which also prevents symlinks from escaping the root. Create creates or truncates a file and requires an existing parent. MkdirAll creates missing parents. Read-only implementations return tgframe.ErrFolderReadOnly for either write operation. Close files and writers returned by Open and Create when finished.

Writable reports granted write access. Path returns an absolute OS path on desktop; other executors return an empty string. The session owns the folder and closes it when replaced or the session ends; use it within the current page run rather than keeping it across selections or sessions.

In Wasm, the dialog uses showDirectoryPicker in a secure context (HTTPS or localhost). Write: true requests readwrite; otherwise it requests read and Writable() is false even if the browser already has broader permission. Cancelling or denying permission keeps the previous selection without rerunning. DefaultDir is ignored in Wasm, and Path() is always empty.

The browser directory handle is passed directly to the Go worker. Reads use File snapshots in chunks and writes use writable streams, committed on Close. No network upload or download is involved. Handles are kept only for the session; reloads require a new selection. Firefox, Safari, and the server Web executor show a disabled button with an explanation and return nil.

Why FolderPicker is disabled in Web

Web executor support is intentionally not scheduled. Go runs on the server, while the browser owns the directory handle; even when both run on the same machine, a handle is not a server path. Implementing the same Folder interface would require browser filesystem requests and replies, run cancellation, session-bound permissions and a new transfer protocol. There is currently no concrete app requiring that added complexity.

For server processing, use MultiFileUpload and DownloadFile, optionally packaging output files into an archive. For reading and writing a local tree in place, use the desktop or Wasm executor. Upload/download does not provide in-place folder access, but it covers the current import/process/export examples without granting a server ongoing access to local files.

The existing disabled-control explanation, "Folder selection is not available in this executor yet.", does not promise future Web support. See the Web FolderPicker decision for the protocol sketch, security and performance assessment, and criteria for reconsidering support. The disabled behavior and message are unchanged by this documentation-only decision.

Example

folder := tgcomp.FolderPicker(p.Main, "Output folder", &tgcomp.FolderPickerConf{
    Write: true,
})
if folder != nil && tgcomp.Button(p.Main, "Export") {
    if err := folder.MkdirAll("results"); err != nil {
        return err
    }
    file, err := folder.Create("results/report.txt")
    if err != nil {
        return err
    }
    if _, err := io.WriteString(file, "Export complete\n"); err != nil {
        _ = file.Close()
        return err
    }
    return file.Close()
}