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.