View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        jan@swi-prolog.org
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2026, SWI-Prolog Solutions b.v.
    7    All rights reserved.
    8
    9    Redistribution and use in source and binary forms, with or without
   10    modification, are permitted provided that the following conditions
   11    are met:
   12
   13    1. Redistributions of source code must retain the above copyright
   14       notice, this list of conditions and the following disclaimer.
   15
   16    2. Redistributions in binary form must reproduce the above copyright
   17       notice, this list of conditions and the following disclaimer in
   18       the documentation and/or other materials provided with the
   19       distribution.
   20
   21    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   22    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   23    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   24    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   25    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   26    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   27    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   28    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   29    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   30    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   31    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   32    POSSIBILITY OF SUCH DAMAGE.
   33*/
   34
   35:- module(desktop,
   36          [ desktop_open/1,             % +Document
   37            desktop_open/2              % +Document, +Options
   38          ]).   39:- autoload(library(apply),[maplist/2,maplist/3]).   40:- autoload(library(error),[must_be/2,existence_error/2]).   41:- autoload(library(lists),[append/3]).   42:- autoload(library(option),[option/2,option/3]).   43:- autoload(library(process),[process_create/3,process_which/2]).   44:- autoload(library(uri),[uri_is_global/1]).   45
   46/** <module> Interact with the desktop environment
   47
   48This library provides access to the  desktop   environment  of  the user.
   49Currently it only provides desktop_open/1,2, which  hands a document over
   50to the application the desktop associates with it.
   51*/
   52
   53%!  desktop_open(+Document) is det.
   54%!  desktop_open(+Document, +Options) is det.
   55%
   56%   Open Document using the application  the   desktop  associates  with
   57%   it, e.g., a PDF viewer for a `.pdf` file or a file browser if
   58%   Document is a directory.  Document is one of
   59%
   60%     - A URL, i.e., text for which uri_is_global/1 is true, such as
   61%       ``https://www.swi-prolog.org`` or ``mailto:bugs@example.com``.
   62%       The URL is passed to the _opener_ unmodified.
   63%     - A file or directory.  This is either a plain file name, a term
   64%       Dir/File or a file _alias_ such as library(lists).  If a plain
   65%       file name does not exist as given it is expanded using
   66%       expand_file_name/2, i.e., ``~``, ``$var`` and wildcards are
   67%       expanded and all matching files are opened.
   68%
   69%   Options processed:
   70%
   71%     - opener(+Command)
   72%       Use Command instead of the platform default.  Command is a
   73%       specification for process_which/2, a term `Command-Args` to
   74%       pass the arguments Args before Document or `win_shell` to use
   75%       the Windows ShellExecute() API.
   76%     - wait(+Boolean)
   77%       If `true`, wait for the _opener_ to complete and raise an
   78%       exception if it fails.  This does __not__ wait for the
   79%       application: openers such as ``xdg-open`` merely hand the
   80%       document to the desktop and complete immediately, normally with
   81%       success regardless of what the desktop does with it.  Default is
   82%       `false`, which starts the opener using the detached(true) option
   83%       of process_create/3.  The option is ignored if `win_shell` is
   84%       used, which never waits.
   85%
   86%   The command to use is taken from  the   first  of  these that yields
   87%   an available command:
   88%
   89%     1. The option opener(Command)
   90%     2. The Prolog flag `desktop_opener` if it is not `default`.  It
   91%        uses the same syntax as the opener(Command) option.
   92%     3. The Windows ShellExecute() API (see win_shell/2)
   93%     4. ``open`` on MacOS
   94%     5. ``cygstart`` (Cygwin), ``termux-open`` (Android/Termux) or
   95%        ``wslview`` (WSL without a Linux desktop)
   96%     6. ``xdg-open`` (freedesktop.org), followed by the desktop
   97%        specific ``gio``, ``gnome-open``, ``kde-open``, ``kde-open5``
   98%        and ``exo-open``
   99%     7. ``wslview``, ``handlr``, ``mimeopen`` or ``run-mailcap``
  100%     8. ``open``, unless we are on Linux, where ``open`` is an alias
  101%        for openvt(1) rather than a document opener.
  102%
  103%   @error existence_error(source_sink, Document) if Document does not
  104%   exist.
  105%   @error existence_error(config, desktop_opener) if no command to open
  106%   documents is available.
  107%   @error process_error(Command, exit(Status)) if wait(true) is used
  108%   and Command does not exit successfully.
  109
  110:- predicate_options(desktop_open/2, 2,
  111                     [ opener(any),
  112                       wait(boolean)
  113                     ]).  114
  115:- create_prolog_flag(desktop_opener, default,
  116                      [ type(term),
  117                        keep(true)
  118                      ]).  119
  120desktop_open(Document) :-
  121    desktop_open(Document, []).
  122
  123desktop_open(Document, Options) :-
  124    document_arguments(Document, Arguments),
  125    opener(Command, Options),
  126    maplist(open_argument(Command, Options), Arguments).
  127
  128%!  document_arguments(+Document, -Arguments) is det.
  129%
  130%   Arguments is the list of URLs and file(File) terms to pass to the
  131%   opener, one invocation per element.
  132
  133document_arguments(Document, [Document]) :-
  134    atomic(Document),
  135    uri_is_global(Document),
  136    !.
  137document_arguments(Document, Arguments) :-
  138    document_files(Document, Files),
  139    maplist(file_argument, Files, Arguments).
  140
  141file_argument(File, file(File)).
  142
  143%!  document_files(+Document, -Files) is det.
  144%
  145%   Files is the list of existing files or directories denoted by
  146%   Document.  Compound  terms are handed  to absolute_file_name/3,
  147%   which deals with both aliases and Dir/File terms.
  148
  149document_files(Spec, [File]) :-
  150    compound(Spec),
  151    !,
  152    (   absolute_file_name(Spec, File,
  153                           [ access(exist),
  154                             file_type(directory),
  155                             file_errors(fail)
  156                           ])
  157    ->  true
  158    ;   absolute_file_name(Spec, File,
  159                           [ access(exist),
  160                             file_errors(error)
  161                           ])
  162    ).
  163document_files(Spec, Files) :-
  164    must_be(atomic, Spec),
  165    atom_string(Name, Spec),
  166    (   access_file(Name, exist)                % also true for directories
  167    ->  Expanded = [Name]                       % do not expand ``$`` and ``*``
  168    ;   catch(expand_file_name(Name, Expanded), error(_,_),
  169              existence_error(source_sink, Spec)),
  170        Expanded \== []
  171    ->  true
  172    ;   existence_error(source_sink, Spec)
  173    ),
  174    maplist(existing_file, Expanded, Files).
  175
  176existing_file(File, Absolute) :-
  177    (   access_file(File, exist)                % also true for directories
  178    ->  absolute_file_name(File, Absolute)
  179    ;   existence_error(source_sink, File)
  180    ).
  181
  182%!  opener(-Command, +Options) is det.
  183%
  184%   Find the command to open a document.  Command is either `win_shell`
  185%   or a term Exe-Args, where Exe is the absolute path of the executable
  186%   and Args are arguments that precede the document.
  187
  188opener(Command, Options) :-
  189    option(opener(Spec), Options),
  190    !,
  191    (   opener_command(Spec, Command0),
  192        resolve_command(Command0, Command)
  193    ->  true
  194    ;   existence_error(program, Spec)
  195    ).
  196opener(Command, _Options) :-
  197    (   current_prolog_flag(desktop_opener, Spec),
  198        Spec \== default,
  199        opener_command(Spec, Command0),
  200        resolve_command(Command0, Command)
  201    ->  true
  202    ;   default_opener(Command0),
  203        resolve_command(Command0, Command)
  204    ->  true
  205    ;   existence_error(config, desktop_opener)
  206    ).
  207
  208opener_command(win_shell, Command) =>
  209    Command = win_shell.
  210opener_command(Exe-Args, Command) =>
  211    must_be(list, Args),
  212    Command = Exe-Args.
  213opener_command(Exe, Command) =>
  214    Command = Exe-[].
  215
  216:- if(current_predicate(win_shell/2)).  217resolve_command(win_shell, Command) =>
  218    Command = win_shell.
  219:- endif.  220resolve_command(Exe-Args, Command) =>
  221    opener_executable(Exe, Path),
  222    Command = Path-Args.
  223resolve_command(_, _) =>
  224    fail.
  225
  226%!  opener_executable(+Command, -Path) is semidet.
  227%
  228%   Path is the absolute file name of Command.  A plain name is searched
  229%   for on ``$PATH``.
  230
  231opener_executable(Command, Path) :-
  232    (   atom(Command),
  233        file_base_name(Command, Command)        % plain name
  234    ->  process_which(path(Command), Path)
  235    ;   process_which(Command, Path)
  236    ).
  237
  238%!  default_opener(-Command) is nondet.
  239%
  240%   Enumerate commands that may be able to  open a document in the order
  241%   we prefer them.  Note that we must not use ``open`` on Linux, where
  242%   this is an alias for openvt(1) rather than a document opener.
  243
  244:- if(current_predicate(win_shell/2)).  245default_opener(win_shell).                      % Windows ShellExecute()
  246:- endif.  247default_opener(open-[]) :-                      % MacOS
  248    current_prolog_flag(apple, true).
  249default_opener(cygstart-[]).                    % Cygwin
  250default_opener('termux-open'-[]).               % Android (Termux)
  251default_opener(wslview-[]) :-                   % WSL without Linux desktop
  252    getenv('WSL_DISTRO_NAME', _),
  253    \+ getenv('DISPLAY', _),
  254    \+ getenv('WAYLAND_DISPLAY', _).
  255default_opener('xdg-open'-[]).                  % freedesktop.org
  256default_opener(gio-[open]).                     % Gnome (GIO)
  257default_opener('gnome-open'-[]).                % Gnome (deprecated)
  258default_opener('kde-open'-[]).                  % KDE
  259default_opener('kde-open5'-[]).
  260default_opener('exo-open'-[]).                  % XFCE
  261default_opener(wslview-[]).                     % WSL (wslu)
  262default_opener(handlr-[open]).                  % handlr
  263default_opener(mimeopen-['-n']).                % Perl File::MimeInfo
  264default_opener('run-mailcap'-[]).               % mailcap(5)
  265default_opener(open-[]) :-                      % Haiku and friends
  266    \+ linux.
  267
  268linux :-
  269    current_prolog_flag(arch, Arch),
  270    sub_atom(Arch, _, _, _, linux),
  271    !.
  272
  273%!  open_argument(+Command, +Options, +Argument) is det.
  274%
  275%   Run Command on a single URL or file name.
  276
  277:- if(current_predicate(win_shell/2)).  278open_argument(win_shell, _Options, Argument) :-
  279    !,
  280    win_shell_argument(Argument, OsArgument),
  281    win_shell(open, OsArgument).
  282:- endif.  283open_argument(Exe-Args, Options, Argument) :-
  284    append(Args, [Argument], Argv),
  285    (   option(wait(Wait), Options, false),
  286        must_be(boolean, Wait),
  287        Wait == true
  288    ->  process_create(Exe, Argv,
  289                       [ stdin(null),
  290                         stdout(null)
  291                       ])
  292    ;   process_create(Exe, Argv,
  293                       [ detached(true),
  294                         stdin(null),
  295                         stdout(null),
  296                         stderr(null)
  297                       ])
  298    ).
  299
  300:- if(current_predicate(win_shell/2)).  301win_shell_argument(file(File), OsFile) :-
  302    !,
  303    prolog_to_os_filename(File, OsFile).
  304win_shell_argument(URL, URL).
  305:- endif.