desktop: the program on the user's desktop
import desktop "gt:desktop" (or the relative path of the submodule). The package is named tk_desktop (the directory is
desktop): a program may have a desktop package of its own, and Odin package names are unique within a build.
The package makes a portable program part of the user's desktop, and installs a copy of it in a folder of its own. Everything is in the user's profile: no administrator rights, nothing system-wide. A program gets:
- The integration: a launcher in the application menu (Start menu, freedesktop.org entry), a desktop shortcut (Windows),
the program on the user's PATH, the icons of the theme (Linux), and for a browser the registration as a candidate browser.
statusreads it from the disk,installwrites it,removetakes it away. - The installed copy:
files_installcopies the executable and what ships beside it, and writes a manifest of the files it placed.uninstallremoves the integration, the system's record and exactly those files. On NixOS the copy is made to run by itself. - A command line:
install,remove,statusanduninstallwithout a window, for scripts and tests. - A watcher: the system tells the program when a place of the integration changes (
watch_start).
Nothing is a preference: the state is read from the disk each time. Removal deletes only what carries the program's mark.
The identity
Identity says what the program is. The backends build every path, key and mark from it, so a second channel of the program
(a development build) has its own files and keys. Its strings and slices are the program's constants: options_clone shares
them with a job on another thread.
| Field | Example | What it is |
|---|---|---|
name | Wren | the menu entry, the shortcut's file name (<name>.lnk), the name in the system's lists |
key | Wren | the registry key and ProgID stem (letters and digits) |
slug | wren | the .desktop file, the icon name, the window class, the default install folder on Linux (lower case, digits, hyphens) |
aumid | FromentSoftware.Wren | the Windows AppUserModelID; set_process_identity gives it to the process's windows |
description, vendor | the menu entry's comment; the publisher in Settings > Apps | |
exe_name | wren.exe | the executable's name in an install folder ("": the slug, with .exe on Windows) |
entry_marker | X-Wren-Integration | the key that marks the .desktop file (begins with X-) |
registry_mark | WrenIntegration | the value that marks a registry key, set to the aumid |
vendor_key | FromentSoftware | Software\<vendor_key>\<key> keeps the program's own values (the PATH mark) |
manifest_name, manifest_header | wren_install.txt | the installed copy's list of files, and its first line |
launcher_mark | (Wren console launcher) | what the console launcher contains ("": the program has none) |
cli_flag | --desktop-integration | the flag parse_cli takes out ("": none) |
env_prefix | WREN_DESKTOP_ | the prefix of the roots roots_from_env reads ("": none) |
url_schemes, url_flag | wren-https, --open-url | the program's own URL schemes, which run "<exe>" <url_flag> "%1" |
uninstall_arguments | --setup | Settings > Apps runs "<exe>" <uninstall_arguments> |
generic_name, categories, keywords | Web Browser, Network;WebBrowser; | the .desktop lines ("": the line is left out) |
nix_runtime_name, nixos_libraries | wren-runtime, glibc... | the Nix profile the installed copy runs with on NixOS (no libraries: the copy is not adapted) |
watch_thread_name | wren-desktop-watch | the watcher's thread |
cleanup_name | wren_cleanup | the stem of the batch file of a deferred deletion ("": the slug) |
scratch_roots | Software\WrenTest_ | the registry keys delete_scratch_root may delete (prefixes) |
install, remove and status refuse an identity that could reach another program's files (Bad_Options): an empty
name, key, slug, aumid, registry_mark, vendor_key or manifest_name, a malformed one, an entry_marker without
X-, a URL scheme that is not lower case, schemes without url_flag, a launcher without launcher_mark.
The options
o := desktop.options_make(IDENTITY) // the running executable, the menu entry on, the system told
o.add_to_path = true
o.icons = ICONS // Linux: the PNGs of the theme, one per size
if r := desktop.install(&o); r.error != .None { log(desktop.error_text(r.error), r.detail) }
s := desktop.status(&o) // .Absent, .Installed or .Repair, part by part
defer desktop.status_destroy(&s)Options holds what an installation has (start_menu, desktop_shortcut, add_to_path, autostart with its autostart_arguments (a program of the notification area starts hidden), register_browser with its
file_types, the launcher bytes, the icons) and where everything goes. install writes the parts that are on and takes
away the ones that are off: applying a change of options is the same call. status says Repair when a wanted part is
missing or incomplete, or when a part runs another executable (stale_exe: the program was moved). describe says it in one
line, parts_wanted and supported_parts say which parts exist here, menu_name, default_apps_name and targets give the
words of this system.
Windows
| Part | What is written |
|---|---|
Launcher | <Start menu>\Programs\<name>.lnk (IShellLink with the AppUserModelID) |
Desktop_Shortcut | <Desktop>\<name>.lnk |
Path | the executable's folder in HKCU\Environment\Path (REG_EXPAND_SZ), marked by PathDir in Software\<vendor_key>\<key>; the console launcher <stem>.com beside <stem>.exe |
Autostart | with autostart: the value <key> of HKCU\Software\Microsoft\Windows\CurrentVersion\Run, "<exe>" <autostart_arguments> |
Registration | only with register_browser: Software\Clients\StartMenuInternet\<key>, Software\RegisteredApplications, the ProgID Software\Classes\<key>HTML, the http and https URL associations, the file types, and Software\Classes\<scheme> for each of url_schemes |
The installed copy goes to %LOCALAPPDATA%\Programs\<name> unless install_dir says otherwise. record_install writes
Software\Microsoft\Windows\CurrentVersion\Uninstall\<key>, which Settings > Apps lists and install_target reads back.
A running program's file cannot be deleted: files_install renames it to .old first, clean_old deletes those at the
next start, and cleanup_later deletes the files a running copy holds after the process ends. open_default_apps opens
ms-settings:defaultapps, where the user picks the default browser: a program that is not a browser does not call it.
The console launcher exists because a program with a window gives the prompt back at once and has no console. The program
provides the launcher's bytes; the launcher must contain launcher_mark, which is how removal tells it from another file.
Linux
| Part | What is written |
|---|---|
Launcher | $XDG_DATA_HOME/applications/<slug>.desktop (desktop_entry_text), marked by its entry_marker line |
Icons | $XDG_DATA_HOME/icons/hicolor/<n>x<n>/apps/<slug>.png, one per icon of the options |
Path | a link ~/.local/bin/<slug> to the executable |
Autostart | with autostart: $XDG_CONFIG_HOME/autostart/<slug>.desktop (the XDG Autostart specification), Exec the executable and autostart_arguments, marked as the desktop entry is |
The entry has MimeType= (http, https, the URL schemes and the file types) only with register_browser, and Exec takes
%U when the program is a browser or has URL schemes. After a change update-desktop-database runs when it is installed.
The installed copy goes to $XDG_DATA_HOME/<slug>; nothing records it but its manifest.
NixOS
A release is linked for a usual Linux: on NixOS it starts only in an FHS sandbox. files_install with adapt makes the
copy run by itself: nix-build makes a profile of nixos_libraries (with patchelf), linked as <install>/nix-runtime (a root
of the garbage collector, listed in the manifest), and patchelf sets the copy's loader and library path to it. A copy in the
Nix store (in_nix_store) is never adapted: its package already runs it. is_nixos reads /etc/os-release.
Open with
handlers_for_file(path) lists the applications the system knows for the type of the file at path (the file need not
exist: its name says its type). The default comes first (Handler.default), then the others by name. Each Handler has
an id and a name for a menu; free the list with handlers_destroy. open_with(path, id) opens the file with one of
them. The system's default application is platform.open_path.
- Linux reads the XDG specifications: the type from shared-mime-info (
mime/globs2andmime/subclassesunder each data directory; everytext/*is alsotext/plain), the applications from the desktop entries underapplications/(theirMimeType; notHidden, notTerminal), andmimeapps.listfor the default and the user's own associations. The id is the desktop file id (org.gnome.TextEditor.desktop).open_withruns itsExecline, the field codes replaced (exec_expand), withplatform.launch_detached. - Windows asks the shell for the recommended handlers of the extension (
SHAssocEnumHandlers, as Explorer's "Open with" shows them). The id is the handler's name;open_withcallsIAssocHandler.Invokewith the file. The default is the handler of the programAssocQueryStringWnames. - Elsewhere: no handler.
exec_arguments(value) splits an Exec value as the specification quotes it, and exec_expand(value, path, name, icon,
desktop_file) makes the command that opens path.
The installed copy
plan := desktop.Install_Plan{identity = IDENTITY, src_exe = process.executable_path(), dir = dir, exe_name = desktop.exe_name_of(IDENTITY),
payload = {"licenses", "readme.txt"}, version = VERSION, channel = "release", adapt = true}
if why := desktop.check_install_dir(dir, IDENTITY.name, probe = true); why != "" { show(why) }
r := desktop.files_install(plan) // then desktop.install and desktop.record_install
info := desktop.installed_info(&o, dir) // version, channel, files, missing
ur := desktop.uninstall(&o, process.executable_path()) // leftovers: files a running copy holdscheck_install_dir refuses system folders, relative paths and names Windows cannot make. compare_versions compares two
version strings part by part. The manifest is <manifest_name> in the folder: version=, channel=, exe= and one file=
per file placed; a hand-edited one never reaches outside its folder.
The command line
parse_cli takes <cli_flag>=install|remove|status|uninstall (or the two-word form, or --install / --uninstall) out of
the arguments; cli_run runs it and writes its report with the procedure it is given (process.console_write). Exit codes:
0 done (status: installed), 1 failed (status: absent), 2 to repair, 3 unreadable, CLI_UNINSTALL_LATER (5) uninstalled
with files left for after the process ends.
The tests' scratch roots
Every place is a field of Options: menu_dir, desktop_dir, environment_root and registry_root (keys under
HKEY_CURRENT_USER), applications_dir, icons_dir, bin_dir, autostart_dir, install_dir. A test points them at a scratch folder and a
scratch key, and turns notify_system off. roots_from_env reads them from <env_prefix>MENU_DIR, <env_prefix>BIN_DIR...
(ROOT_NAMES), so a program's own end-to-end tests run it on scratch places; any of them set turns the notification off.
delete_scratch_root deletes a scratch registry key, and only one under the identity's scratch_roots.
The package's tests (odin run tools/run_tests -- desktop_pkg) use an identity of their own (Sample): the Linux backend on
scratch folders, the Windows backend on HKCU\Software\TkDesktopTest_<id>, and on NixOS a copy of bash adapted with a real
runtime. TK_TEST_DESKTOP_REAL=1 adds one round trip on the real Windows desktop.