launch

tape
import launch from "launch";
import ui from "ui";
import io from "io";

fn handed_over(args: []const []const u8) {
    // a second start, or a chat://... link clicked in the browser
    for (i in 0..args.len) { io.println("handed: {args[i]}"); }
    ui.activate_window();         // opened again if closed, brought to the front
}

component App { view { ui.Column {} } }

fn main() -> i32 {
    let c: launch.Claim = launch.claim("com.example.chat", handed_over) or { return 1; };
    if (c == launch.Claim.HandedOver) { return 0; }     // the running one has our arguments
    launch.register_scheme("chat") or { io.println("no scheme: {err}"); };
    ui.run_window(App {}, ui.Window { title: "Chat"; keep_running: true; }) or { return 1; };
    launch.release();
    return 0;
}

One running instance

claim(app_id, on_handover) makes the program the one instance of app_id for this user’s session:

  • the first program to claim the name gets Claim.Owner, and from then on every later instance’s arguments come to on_handover in the ui loop;
  • a later one gets Claim.HandedOver: its arguments (without the program’s name) are already with the owner, and it should exit.

claim_args(app_id, args, on_handover) hands over args instead of the program’s own. release() gives the name up (the next instance to start owns it), owned() says whether the program owns one, and poll() delivers waiting handovers now (the ui loop calls it for you).

An app id is 1 to 64 letters, digits, ., - and _ (a reverse domain name is a good one); anything else traps, as does claiming a second name. The arguments of one handover are at most MAX_HANDOVER bytes (60,000, 4 more per argument): TooLong past that. Under the hood it is a named pipe on Windows and a Unix-domain socket on Linux and macOS, only this user can write to it: in $XDG_RUNTIME_DIR, else in a directory of the user’s own in the temporary directory (tape-launch-<uid>, made with mode 0700). If that directory is not this user’s alone (another user made it first, it is a link, or it is open to others), claim refuses with NotPrivate rather than hand arguments to someone else.

A URL scheme of your own

register_scheme("chat") makes the program the handler of chat: URLs for this user: clicking one starts it with the URL as its one argument, and with claim the running instance gets it. unregister_scheme undoes it. A scheme is a lowercase letter, then letters, digits, +, - or .; anything else traps.

WindowsHKEY_CURRENT_USER\Software\Classes\<scheme>, the command "<the program>" "%1"
Linux~/.local/share/applications/tape-scheme-<scheme>.desktop with MimeType=x-scheme-handler/<scheme>, made the default with xdg-mime
macOSUnsupported: the scheme belongs in an app bundle’s Info.plist, which tape build does not make yet

open_url(url) opens a link in the user’s browser (or a mailto: in the mail program). Only http:, https: and mailto: URLs with no spaces or control characters are opened; anything else is InvalidUrl, because a link from a message could otherwise start any handler the system has. is_openable(url) asks first. ShellExecuteExW on Windows, NSWorkspace on macOS; on Linux the desktop portal’s OpenURI through dbus (which also works from a Flatpak sandbox), else xdg-open.

LaunchErrorWhen
UnsupportedThe platform can’t (macOS schemes, Uusi)
InvalidUrlNot an http:, https: or mailto: URL
TooLongThe arguments don’t fit one handover
NotPrivateThe socket’s directory under the temporary directory is not this user’s own (Linux and macOS without $XDG_RUNTIME_DIR)
FailedThe system refused

Last modified: