Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dialogs

Perry provides native dialog functions for file selection, alerts, and sheets. Every snippet below is excerpted from docs/examples/ui/dialogs/snippets.ts — CI compiles and links the file on every PR, so the API drawn here is the API the runtime exposes.

All file dialogs are callback-based (the OS-modal panel is non-blocking on Apple platforms, so a synchronous return wouldn’t be possible without freezing the app’s run loop). The callback receives an empty string when the user cancels.

File Open Dialog

function pickFile(): void {
    openFileDialog((path: string) => {
        if (path.length > 0) {
            console.log(`Selected: ${path}`)
        } else {
            console.log("Open dialog cancelled")
        }
    })
}

Folder Selection Dialog

function pickFolder(): void {
    openFolderDialog((path: string) => {
        if (path.length > 0) {
            console.log(`Selected folder: ${path}`)
        }
    })
}

Save File Dialog

function pickSaveTarget(): void {
    saveFileDialog((path: string) => {
        if (path.length > 0) {
            console.log(`Will save to: ${path}`)
        }
    }, "untitled", "txt")
}

saveFileDialog(callback, defaultName, extension) pre-fills the name field with defaultName.<extension>.

Alert

Display a native alert dialog:

function showSimpleAlert(): void {
    alert("Operation Complete", "Your file has been saved successfully.")
}

alert(title, message) shows a modal alert with an OK button.

Alert with Buttons

function confirmDelete(): void {
    alertWithButtons(
        "Delete Item?",
        "This action cannot be undone.",
        ["Cancel", "Delete"],
        (index: number) => {
            if (index === 1) {
                console.log("user confirmed delete")
            }
        },
    )
}

alertWithButtons(title, message, buttons, callback) invokes the callback with the 0-based index of the button the user clicked. By convention put a destructive label last and check the index in the callback.

Sheets

Sheets are modal panels attached to a window. Build the body, hand it (with a size) to sheetCreate, then sheetPresent it. To dismiss programmatically, keep the handle around and call sheetDismiss(handle):

function showSheet(): void {
    let sheet = 0
    const body = VStack(16, [
        Text("Sheet Content"),
        Button("Close", () => sheetDismiss(sheet)),
    ])
    sheet = sheetCreate(body, 320, 200)
    sheetPresent(sheet)
}

Platform Notes

DialogmacOSiOSWindowsLinuxWeb
File OpenNSOpenPanelUIDocumentPickerIFileOpenDialogGtkFileChooserDialog<input type="file">
File SaveNSSavePanelIFileSaveDialogGtkFileChooserDialogDownload link
FolderNSOpenPanelIFileOpenDialogGtkFileChooserDialog
AlertNSAlertUIAlertControllerMessageBoxWMessageDialogalert()
SheetNSSheetModal VCModal DialogModal WindowModal div

Complete Example: minimal text editor

A real program that wires openFileDialog and saveFileDialog into a state-bound TextField:

// demonstrates: file-open / save dialogs wired to a tiny text editor
// docs: docs/src/ui/dialogs.md
// platforms: macos, linux, windows

import {
    App,
    VStack, HStack,
    Text, Button, TextField,
    State,
    openFileDialog, saveFileDialog, alert,
} from "perry/ui"
import { readFileSync, writeFileSync } from "fs"

const content = State("")
const filePath = State("")

App({
    title: "Text Editor",
    width: 800,
    height: 600,
    body: VStack(12, [
        HStack(8, [
            Button("Open", () => {
                openFileDialog((path: string) => {
                    if (path.length === 0) return
                    filePath.set(path)
                    content.set(readFileSync(path, "utf-8") as string)
                })
            }),
            Button("Save As", () => {
                saveFileDialog((path: string) => {
                    if (path.length === 0) return
                    writeFileSync(path, content.value)
                    filePath.set(path)
                    alert("Saved", `File saved to ${path}`)
                }, "untitled", "txt")
            }),
        ]),
        Text(filePath.value === "" ? "No file open" : `File: ${filePath.value}`),
        TextField("Start typing...", (value: string) => content.set(value)),
    ]),
})

Next Steps