Desktop APIs
MyPHP wraps the parts of the operating system desktop apps use most, for
apps whose windows show web pages, native UI or both. They are static
classes of MyPHP, called on the main thread, from App::whenReady() on.
Dialogs
Dialog shows the system's file and message dialogs. Its methods wait
until the user answers, the app drawing and taking events meanwhile:
call them from a menu item, a bound method or App::later(). parent
attaches a dialog to a window, as a sheet on macOS.
// Pick files.
$paths = Dialog::open(new OpenDialogOptions(
parent: $win,
title: 'Import',
filters: [new FileFilter('Images', ['png', 'jpg'])],
multiple: true,
)); // [] when canceled
// Choose where to save.
$path = Dialog::save(new SaveDialogOptions(
parent: $win,
defaultPath: 'notes.json',
)); // '' when canceled
// Ask a question.
$res = Dialog::message(new MessageOptions(
parent: $win,
type: 'warning',
message: 'Delete 3 notes?',
detail: 'You cannot undo this.',
buttons: ['Cancel', 'Delete'],
destructiveButtons: [1],
checkboxLabel: "Don't ask again",
));
if ($res->button === 1) { /* Delete */ }
// Report an error.
Dialog::error('Import failed', $e->getMessage());
OpenDialogOptions also pick directories (directory), show hidden files
and, on macOS, a message and a New Folder button. In message dialogs Enter
activates defaultButton and Escape cancelButton (by default the first
button labeled Cancel or No); on macOS Cmd+. cancels too.
destructiveButtons sets apart buttons that delete or can't be undone.
Dialog::prompt() asks for a line of text.
Notifications
$n = new Notification(new NotificationOptions(
title: 'Export finished',
body: 'notes.json was saved to Documents.',
));
$n->onClick(fn() => $win->focus());
$n->show();
id replaces a notification of the same id still showing, group groups
them in the notification center, silent plays no sound and subtitle
shows under the title; Notification::supported() tells whether the
system shows them, close() takes one away and Notification::clear()
all of the app's. App::onNotificationClick() hears clicks on
notifications the app showed before it was started again.
Clipboard
Clipboard::writeText('hello');
$text = Clipboard::readText();
Clipboard::writeHTML('<b>hello</b>');
Clipboard::writeImage($png); // a PNG
$img = Clipboard::readImage();
$formats = Clipboard::availableFormats();
Clipboard::clear();
Pages can use the web's navigator.clipboard too.
Clipboard::write($data) and Clipboard::read(...$formats) share the
drag and drop model of MyPHP\Transfer\Data: ordered items, alternative
representations, file lists, custom MIME formats, and lazy providers.
formats() discovers portable names without requesting bytes;
readFormat(), readFiles(), writeFiles(), and flush() provide
explicit negotiation, file transfer, and persistence. See [Clipboard and
drag data](data-transfer.md). The clipboard example
demonstrates these operations.
The shell
Shell::openExternal('https://example.com'); // the default browser
Shell::openPath('/Users/ada/report.pdf'); // the default app for the file
Shell::showItemInFolder($path); // Finder, the file manager
Shell::trashItem($path); // to the trash
Shell::beep();
trashItem() never deletes for good: when the item cannot go to the
trash it throws and leaves the item where it is.
fileIcon() returns the icon the file manager shows for a file, a folder
or an application, as a PNG of the size asked for in pixels: pass the size
in points times the display's scale factor.
$png = Shell::fileIcon('/Applications/Safari.app', 64);
On macOS it is Finder's icon (NSWorkspace); on Linux the icon of the
file's content type from the icon theme, and for a desktop entry, the icon
of its application.
installedApps() lists the applications installed on the computer,
sorted by name, as a launcher shows them, and launchApp() starts one, or
brings it to the front:
$apps = Shell::installedApps();
foreach ($apps as $app) {
echo $app->name, ' ', $app->id, "\n";
}
$icon = Shell::fileIcon($apps[0]->path, 64);
Shell::launchApp($apps[0]);
Each app has the name the system shows, in the user's language, an id
and the path whose icon fileIcon() draws: the .app bundles of
/Applications and the system's on macOS, with the bundle identifier as
their id; the desktop entries menus show on Linux, with the desktop file
ID. Shell::appIcon($size) is the app's own icon.
Displays
Screen describes the connected displays, in points with the origin at
the top-left corner of the primary display:
$primary = Screen::primaryDisplay();
[$bounds, $workArea, $scale] = [$primary->bounds, $primary->workArea, $primary->scaleFactor];
foreach (Screen::displays() as $d) {
echo $d->label, ' ', $d->bounds->width, '×', $d->bounds->height, "\n";
}
$p = Screen::cursorScreenPoint();
$under = Screen::displayNearestPoint($p);
A Display has its bounds, its workArea (without the menu bar and the
Dock), its scaleFactor, rotation, and whether it is internal or the
primary one. displayMatching($rect) returns the display a rectangle
is mostly on, and onDisplaysChanged() hears displays come, go and
change.
Dark mode
$dark = Theme::isDark();
Theme::onUpdated(fn() => applyTheme(Theme::isDark()));
Theme::setSource(ThemeSource::Dark); // or Light; System follows the system
MyPHP\Theme is the appearance of the app's windows and pages; native UI
draws with MyPHP\UI\Theme, which follows it (see
themes). Pages follow it with
prefers-color-scheme.
Accent color and accessibility
Theme::preferences() returns the settings of the desktop that the app's
look follows: the accent color the user chose, and whether they asked
for reduceMotion, reduceTransparency or highContrast, and the
textScale. Theme::onUpdated() hears them change too. Pages get them as
window.myphp preferences, and native UI as
Power
Power::onSuspend(fn() => pauseSync());
Power::onResume(fn() => resumeSync());
$release = Power::keepAwake('Exporting video', display: false); // keeps the system awake
// … when done:
$release();
$onBattery = Power::isOnBattery();
$level = Power::batteryLevel(); // null without a battery
$idle = Power::idleTime(); // seconds since the user's last input
onLockScreen() and onUnlockScreen() hear the screen lock.
keepAwake() with display: true keeps the display on too, as a video
player does.
Now Playing and media keys
NowPlaying tells the system what the app plays, which shows in Control
Center and on the lock screen, and takes the media keys and the AirPods'
controls:
NowPlaying::set(new NowPlayingInfo(
title: 'Song',
artist: 'Artist',
duration: 215,
position: 12,
rate: 1,
commands: MediaCommand::PLAY | MediaCommand::PAUSE | MediaCommand::TOGGLE | MediaCommand::NEXT | MediaCommand::SEEK,
));
NowPlaying::onCommand(function (int $cmd, float $position) use ($player) {
match ($cmd) {
MediaCommand::PLAY => $player->play(),
MediaCommand::PAUSE => $player->pause(),
default => null,
};
});
NowPlaying::setPlayback(PlaybackState::Paused, 30.5);
NowPlaying::clear();
Network
$status = Network::status(); // online, metered
Network::onChanged(function (NetworkStatus $s) {
if (!$s->online) {
showOffline();
}
});
online tells whether the computer has a route to the internet, and
metered whether the connection costs, as a phone's hotspot.
Proxies
MyPHP's own requests (MyPHP\Http's, the updater's, those of the fetch
and WebSocket plugins) go through the proxy the user set in the system's settings, as
the app's pages do, with its exceptions and setup scripts. Loopback hosts
are reached directly. When any of HTTP_PROXY, HTTPS_PROXY,
ALL_PROXY or NO_PROXY (or its lowercase name) is set, the environment
decides instead, as curl reads it. The system's answer is kept for 5
seconds, so a change of the settings applies without restarting the app.
Processes
MyPHP\Process runs a program without blocking the app: its output is
read as it comes, polled on the run loop, and a function gets all of
stdout, stderr and the exit code once it ends.
<!-- no-test -->
use MyPHP\Process;
$p = Process::run(['git', 'log', '--oneline', '-20'], '', function (string $out, string $err, int $code) use ($app, $win) {
$win->update(function () use ($app, $out, $err, $code) {
$app->log = $code === 0 ? explode("\n", trim($out)) : ["git failed: $err"];
});
});
// later, to stop it: $done is then not called
$p->kill();
Process::run($argv, $stdin, $done):$argvis the program and its
arguments, not a shell line (no quoting, no globbing); $stdin is
written to its standard input, then closed ('' for none); `$done(string
$stdout, string $stderr, int $exitCode)` runs on the main thread once it
ended. A program that cannot be started throws a RuntimeException.
$p->ended()tells whether it ended and$doneran;$p->kill()ends
it now (SIGTERM) without calling $done.
- The output is handed over whole at the end: for progress as it runs,
have the program write to a file and read that, or use the APIs made for
the job, as MyPHP\Http for HTTP requests
Secrets
Secrets keeps passwords and tokens in the system's keychain (the
Keychain on macOS, the Secret Service on Linux), under the app's name:
Secrets::set('github-token', $token);
try {
$token = Secrets::get('github-token');
} catch (SecretNotFound) {
$token = null;
}
Secrets::delete('github-token');
$work = Secrets::service('com.example.work'); // under another service
Watching files
App::watchFiles() watches a file or a directory for changes, which a
fiber reads as they come, debounced:
App::go(function () {
$w = App::watchFiles('/Users/ada/notes', new FileWatchOptions(recursive: true, debounce: 0.1));
while (($events = $w->next()) !== null) {
foreach ($events as $e) { // FileEvent: op, path, oldPath, isDir
reindex($e->path);
}
}
});
ignore leaves out paths, maxEntries bounds what a recursive watch
holds, and hintsOnly reports that something changed without listing
what. The watch plugin gives pages the same.
Global shortcuts
A global shortcut works while the app is in the background, as a
launcher's:
App::whenReady(function () {
GlobalShortcut::register('CmdOrCtrl+Shift+Space', fn() => toggleLauncher());
});
GlobalShortcut::unregister('CmdOrCtrl+Shift+Space');
They take accelerators; isRegistered() and
unregisterAll() go with them. On Linux under Wayland, global shortcuts
need the desktop's portal.