16 May 2003 auspex   » (Journeyer)

Frustrated

It is a trivial matter to write a "remote control" for one's X application. Consider a common problem: how to make the application unique to a display[1]. The most trivial solution causes the program to exit if another instance is running. This is the basic[2] code:

void
ensure_uniqueness (Display *display)
{
  Atom app_id;
  app_id = XInternAtom (display, "_MY_APP_ID", False);
  if (XGetSelectionOwner (display, app_id) == None) {
    Window app_window;
    app_window = XCreateSimpleWindow (display, DefaultRootWindow (display),
                                      0, 0, 1, 1, 0, 0, 0);
    XSetSelectionOwner (display, app_id, app_window, CurrentTime);
  } else {
    exit (EXIT_FAILURE);
  }
}

Just exiting will befuddle users, so it'd be good to do something visible. The most trivial visible thing is to present an error alert, but that's annoying and blockheaded. Suppose it suffices to present an existing program window to the user. Somehow the second instance (P2) must tell the first instance (P1), or the window manager (WM), to do this. Because P2 cannot get the correct window by itself, to tell the WM what to do it has to get the correct window from P1. It is simpler to tell P1 what to do and let it handle the rest. For P2, ensure_uniqueness() calls this instead of exit():

void
call_for_presentation (Display *display, Atom app_id)
{
  Atom   present_window;
  Window dummy_window;
  present_window = XInternAtom (display, "_MY_APP_PRESENT_WINDOW", False);
  dummy_window = XCreateSimpleWindow (display, DefaultRootWindow (display),
                                      0, 0, 1, 1, 0, 0, 0);
  XConvertSelection (display, app_id, present_window,
                     None, dummy_window, CurrentTime);
}
When P1 receives the SelectionRequest event, it will filter this ordinarily and on finding what has been requested it will present some window and send a SelectionNotify event to P2. This is using a side effect target and is described in section 2.6.3 of the ICCCM.

Just presenting an existing window will not suffice for most applications, though it's fine for simple utilities and control panels. Often an application will manipulate some document, so it will be useful for P2 to tell P1 to open a document.[3] This is basically passing a string, so command line arguments and more can be passed the same way. Again, a side effect target is used, but this time there is data in the property specified for XConvertSelection(). (This property was None in the previous code.)

void
call_for_open (Display *display, Atom app_id, const char *filename)
{
  Atom   _my_app_open;
  Window data_window;
  Atom   data_property;
  Atom   data_property_type;
  _my_app_open = XInternAtom (display, "_MY_APP_OPEN", False);
  data_window = XCreateSimpleWindow (display, DefaultRootWindow (display),
                                     0, 0, 1, 1, 0, 0, 0);
  data_property = XInternAtom (display, "_MY_APP_DATA", False);
  data_property_type = XA_STRING;
  XChangeProperty (display, data_window,
                   data_property, data_property_type, 8,
                   PropModeReplace,
                   filename, strlen(filename));
  XConvertSelection (display, app_id, _my_app_open,
                     data_property, data_window, CurrentTime);
}
Now when P1 recieves the SelectionRequest event, it will take the additional step of retrieving the data with XGetWindowProperty().

The entire remote control system can be built with one selection target and a string passed for interpretation. It could even return a result. I do not know if that would be better than using multiple side effect targets as commands.

What I've described thusfar is a remote control system for one program. It it allows one instance of an executable to control another. Both transmitter and receiver are in the same executable file, so there is little need for discovery, versioning, or even sensible names. However, if another version of the same program uses the same uniqueness atom, there could be problems. This makes the remote control less trivial.

Versioning is convenient because it allows for a simple check; even with a discovery method, it is worthwhile. One obvious way to handle it is by noting the version in the app id, but that would eliminate the benefits of uniqueness. Instead, it is better to provide a target which will return the version. The target VERSION, indicating an ICCCM version, is required of ICCCM-compliant window managers.[4] For a window manager to have a versioned remote control, it must use another target. For common remote control targets to be standardized for all apps, another target must be used, or window managers must be excepted.

The ICCCM-required target TARGETS provides a list of available targets. This is suitable for discovering whether a program has a particular remote control command, but not what it does or how. By always using a target that does not change with application versions, an application can provide a dictionary of its remote control commands.

Furthermore, if every application providing a remote control uses the same dictionary target, then it is possible to build a "universal" remote control.

More to come, including an explanation of the title.

Notes:

[1] Subdomains of uniqueness are possible, such as a particular screen of a display, or a particular host connected to that display. This method depends on the display and is not suitable for making the program unique to -say- a host. Other methods, such as special files, exist for that.

[2] The code ignores a lot. The created Window isn't returned in any way; though a later call to XGetSelectionOwner() might retrieve it. No events are handled. Using CurrentTime is wrong. Etc.

[3] There is a problem here in that P1 and P2 may be on different hosts without access to the same documents. The simple and incomplete solution is to use URIs like file://host/path/to/file to avoid false matches and fail when the file cannot be opened.

[4] This was a bad choice. The ICCCM version target should have been something like ICCCM_VERSION and the VERSION target left for application versions.

Latest blog entries     Older blog entries

New Advogato Features

New HTML Parser: The long-awaited libxml2 based HTML parser code is live. It needs further work but already handles most markup better than the original parser.

Keep up with the latest Advogato features by reading the Advogato status blog.

If you're a C programmer with some spare time, take a look at the mod_virgule project page and help us with one of the tasks on the ToDo list!