The hyperlinks in text printed to terminal emulators [1] can have a wide range of applications. When the user selects to open the hyperlink, the following types of actions are practically useful:
(A) The action can be to open a browser window. This is supported through the 'http' and 'https' protocols. (B) The action can be to open a file, in whatever application is customary for the type of file. This is supported through the 'file' protocol. Typically, the program will be chosen based on the suffix of the file name (e.g. a text editor for *.c files, a video viewer for *.mp4 files). (C) The action can be to send some information to a running application. Examples are: - An interactive program has a 'help' command that prints a set of available commands. When the user selects one of these command, the command gets added to the prompt. This reduces the amount of necessary typing. - A Lisp system prints some objects, in normal Lisp syntax (often in abbreviated form: #(a b c ...) or #<OBJECT XY>) with hyperlinks that identify the object. When the user selects such a hyperlink. the Lisp system opens an "inspector" that shows details of the object. (D) The action can be to open an application with a specific set of command-line arguments. Examples are: - The 'grep' program shows references into disk files. When the user selects one of these references, a text editor opens the referenced file at the particular line. - A program presents the table of contents of an audio CD. When the user selects one of the items, the audio player is launched, and it starts playing the audio file at the particular time within the file. - Any program that produces localized messages: When the user selects one of the localized messages in the terminal output, a PO file editor opens, at the point where the translation of the particular message is defined, and the user can correct the translation and save the modified PO file. For the use case (C), I would like to suggest a new protocol. The purpose of standardizing this protocol is that different terminal emulators behave the same way. Specification ------------- The name of the protocol should be 'appsocket'. The syntax of the URI is appsocket://hostname:port/payload . When the hostname is equal to the machine's gethostname(), the connection should be done to localhost. The port is in the range 1..65534. The payload is defined by the application. It is recommended that slashes are used to separate different parts of the payload. It is recommended that one part of the payload is a nonce token [2], for security reasons. The terminal emulator's action, when the user opens a hyperlink with this protocol, is to open a TCP/IP client socket to the given host name and port and send the "/payload" string (including the starting slash), followed by a newline, to that socket, then finally close the client socket. Rationale --------- Experience with GNU poke [3] has shown that the model (C) provides very satisfying user interactions. Use case (C) cannot be based on (B), because if the action would be to open a browser window that triggers some secondary action, the user would be required to manually close this browser window, which would be annoying. (Browser windows can be closed by JavaScript only if they have been opened by JavaScript.) Why a socket? Because Koblinger's gist [1] considers the possibility to use hyperlinks through ssh, that is, have the terminal emulator run on a different machine than the program that emits the hyperlink. A TCP/IP socket is the easiest way to enable such communication between machines. Why the special convention regarding the machine's gethostname()? Because in the frequent case that the terminal emulator and the program run on the same machine, it avoids making DNS related queries. This not only saves elapsed time, it also makes the communication independent of the contents of the /etc/hosts and /etc/resolv.conf files. This convention already exists for 'file' URLs in [1], is implemented in GNU 'ls', and works fine. Note ---- The terminal emulator's action can easily be embodied in a program, which I've called 'appsocket-client' (attached). A terminal emulator can simply invoke this program, passing it the URI on standard input. This way, the protocol logic does not need to be hardwired into any terminal emulator. The only logic a terminal emulator needs to contains is: "If the URI starts with 'appsocket:', pass it to the 'appsocket-client' program." Very simple. Relation to (D) --------------- It would in theory be possible to base the 'appsocket' protocol on the 'appstart' protocol. But this looks like overkill because - it would be sensitive to user customization, where no user customization is needed nor useful, - it would require two successive program invocations instead of one, at each interaction - making things slower. Relation to other protocols --------------------------- The intended use case is not covered by any of the existing specifications [4]. [1] https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda [2] https://en.wikipedia.org/wiki/Cryptographic_nonce [3] http://jemarch.net/poke.html [4] https://www.freedesktop.org/wiki/Specifications/
/* appsocket-client - Program that dispatches appsocket URIs to the particular program. */ /* Copyright (C) 2019-2020 Free Software Foundation, Inc. */ /* This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* Written by Bruno Haible <br...@clisp.org>, 2011, 2019. */ #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/uio.h> #define VERSION "0.0" #define MAX_URI_LENGTH 2080 /* see Koblinger's gist */ /* Creates a client socket, by connecting to a server on the given port. */ static int create_client_socket (const char *hostname, int port) { /* Create a client socket. */ int client_socket = socket (PF_INET, SOCK_STREAM, IPPROTO_TCP); if (client_socket < 0) { fprintf (stderr, "Failed to create socket.\n"); exit (10); } /* Connect to the server process at the specified port. */ { struct sockaddr_in addr; memset (&addr, 0, sizeof (addr)); /* needed on AIX and OSF/1 */ addr.sin_family = AF_INET; inet_pton (AF_INET, hostname, &addr.sin_addr); addr.sin_port = htons (port); if (connect (client_socket, (const struct sockaddr *) &addr, sizeof (addr)) < 0) { fprintf (stderr, "Failed to open socket %s:%d\n", hostname, port); exit (11); } } return client_socket; } static void usage (FILE *out) { printf ("Usage: printf '%%s' URI | appsocket-client\n"); } int main (int argc, char *argv[]) { if (argc > 1) { if (strcmp (argv[1], "--help") == 0) { usage (stdout); exit (0); } if (strcmp (argv[1], "--version") == 0) { printf ("appsocket-client " VERSION "\n"); exit (0); } fprintf (stderr, "Invalid argument '%s'\n", argv[1]); usage (stderr); exit (1); } char uri[MAX_URI_LENGTH + 1]; size_t n = fread (uri, 1, sizeof (uri), stdin); if (n == 0) { fprintf (stderr, "No URI given on standard input.\n"); exit (2); } if (n > MAX_URI_LENGTH) { fprintf (stderr, "URI too long.\n"); exit (3); } /* NUL-terminate, for simplicity. */ uri[n] = '\0'; /* Verify the URI starts with "appsocket:". */ if (!(n >= 10 && memcmp (uri, "appsocket:", 10) == 0)) { fprintf (stderr, "URI does not have the 'appsocket' protocol.\n"); exit (4); } /* Dissect the URI and verify its syntax: appsocket://hostname:port/payload. */ if (!(uri[10] == '/' && uri[11] == '/')) { fprintf (stderr, "URI syntax error.\n"); exit (5); } char *colon_pos = strchr (uri + 12, ':'); char *slash_pos = strchr (uri + 12, '/'); if (!(colon_pos != NULL && (slash_pos == NULL || colon_pos < slash_pos))) { fprintf (stderr, "URI syntax error.\n"); exit (5); } const char *hostname = uri + 12; *colon_pos = '\0'; if (*hostname == '\0') { fprintf (stderr, "URI syntax error.\n"); exit (5); } char *port_str = colon_pos + 1; if (slash_pos != NULL) *slash_pos = '\0'; long port; char *end_of_port; errno = 0; port = strtol (port_str, &end_of_port, 10); if (*end_of_port != '\0' || errno != 0 || port <= 0 || port >= 65535) { fprintf (stderr, "URI syntax error.\n"); exit (5); } char empty_string[1] = { '\0' }; char *payload = (slash_pos != NULL ? slash_pos + 1 : empty_string); /* Open the port on the hostname. */ char my_hostname[64]; if (gethostname (my_hostname, sizeof (my_hostname)) >= 0 && strcmp (my_hostname, hostname) == 0) /* Use localhost instead of my_hostname. */ hostname = "localhost"; /* Send the payload and a newline. */ int client_socket = create_client_socket (hostname, port); size_t remaining = strlen (payload) + 1; payload[remaining - 1] = '\0'; do { ssize_t written = write (client_socket, payload, remaining); if (written <= 0) { fprintf (stderr, "Failed to write to socket %s:%d\n", hostname, (int)port); exit (12); } payload += written; remaining -= written; } while (remaining > 0); exit (0); }
_______________________________________________ xdg mailing list xdg@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/xdg