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
| Dialog | macOS | iOS | Windows | Linux | Web |
|---|---|---|---|---|---|
| File Open | NSOpenPanel | UIDocumentPicker | IFileOpenDialog | GtkFileChooserDialog | <input type="file"> |
| File Save | NSSavePanel | — | IFileSaveDialog | GtkFileChooserDialog | Download link |
| Folder | NSOpenPanel | — | IFileOpenDialog | GtkFileChooserDialog | — |
| Alert | NSAlert | UIAlertController | MessageBoxW | MessageDialog | alert() |
| Sheet | NSSheet | Modal VC | Modal Dialog | Modal Window | Modal 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
- Menus — Menu bar and context menus
- Multi-Window — Multiple windows
- Events — User interaction events