/*
 * XBrlAPI - A background process tinkering with X for proper BrlAPI behavior
 *
 * Copyright (C) 2003-2022 by Samuel Thibault <Samuel.Thibault@ens-lyon.org>
 *
 * XBrlAPI comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

/* Compile with:
 * gcc -O3 -Wall xbrlapi.c -L/usr/X11R6/lib -lbrlapi -lX11 -o xbrlapi
 */

#include "prologue.h"

#include <stdio.h>
#include <stdarg.h>
#include <signal.h>
#include <string.h>

#ifdef HAVE_LANGINFO_H
#include <langinfo.h>
#endif /* HAVE_LANGINFO_H */

#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#else /* HAVE_SYS_SELECT_H */
#include <sys/time.h>
#endif /* HAVE_SYS_SELECT_H */

#ifdef HAVE_ICONV_H
#include <iconv.h>
#endif /* HAVE_ICONV_H */

#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/XKBlib.h>
#include <X11/keysym.h>

#undef CAN_SIMULATE_KEY_PRESSES
#if defined(HAVE_X11_EXTENSIONS_XTEST_H) && defined(HAVE_X11_EXTENSIONS_XKB_H)
#include <X11/extensions/XTest.h>
#define CAN_SIMULATE_KEY_PRESSES
#else /* HAVE_X11_EXTENSIONS_XTEST_H && HAVE_X11_EXTENSIONS_XKB_H */
#warning key press simulation not supported by this build - check that libxtst has been installed
#endif /* HAVE_X11_EXTENSIONS_XTEST_H && HAVE_X11_EXTENSIONS_XKB_H */

#include "xsel.h"

#define BRLAPI_NO_DEPRECATED
#include "brlapi.h"

#include "options.h"

#define debugf(fmt, ...) do { if (verbose) fprintf(stderr, fmt, ## __VA_ARGS__); } while (0)

/******************************************************************************
 * option handling
 */

static char *auth;
static char *host;
static char *xDisplay;
static int no_daemon;
static int quiet;
static int verbose;
static int xkb_major_opcode;

static int brlapi_fd;

static void *clipboardData;

BEGIN_OPTION_TABLE(programOptions)
  { .word = "brlapi",
    .letter = 'b',
    .argument = strtext("[host][:port]"),
    .setting.string = &host,
    .description = strtext("BrlAPI host and/or port to connect to")
  },

  { .word = "auth",
    .letter = 'a',
    .argument = strtext("scheme+..."),
    .setting.string = &auth,
    .description = strtext("BrlAPI authorization/authentication schemes")
  },

  { .word = "display",
    .letter = 'd',
    .argument = strtext("display"),
    .setting.string = &xDisplay,
    .description = strtext("X display to connect to")
  },

  { .word = "no-daemon",
    .letter = 'n',
    .setting.flag = &no_daemon,
    .description = strtext("Remain a foreground process")
  },

  { .word = "quiet",
    .letter = 'q',
    .setting.flag = &quiet,
    .description = strtext("Do not write any text to the braille device")
  },

  { .word = "verbose",
    .letter = 'v',
    .setting.flag = &verbose,
    .description = strtext("Write debugging output to stdout")
  },
END_OPTION_TABLE

/******************************************************************************
 * error handling
 */

static void api_cleanExit(void) {
  if (brlapi_fd>=0)
  {
    close(brlapi_fd);
    brlapi_fd=-1;
  }
}

/* dumps errors which are fatal to brlapi only */
static void fatal_brlapi_errno(const char *msg, const char *fmt, ...) {
  brlapi_perror(msg);
  if (fmt) {
    va_list va;
    va_start(va,fmt);
    vfprintf(stderr,fmt,va);
    va_end(va);
  }
  api_cleanExit();
}

static void exception_handler(int error, brlapi_packetType_t type, const void *packet, size_t size) {
  char str[0X100];
  brlapi_strexception(str,0X100, error, type, packet, size);
  fprintf(stderr, "xbrlapi: BrlAPI exception: %s\nDisconnecting from brlapi\n", str);
  api_cleanExit();
}

/* dumps errors which are fatal to the whole xbrlapi */
static void fatal_errno(const char *msg, const char *fmt, ...) {
  perror(msg);
  if (fmt) {
    va_list va;
    va_start(va,fmt);
    vfprintf(stderr,fmt,va);
    va_end(va);
  }
  exit(PROG_EXIT_FATAL);
}

static void fatal(const char *fmt, ...) {
  if (fmt) {
    va_list va;
    va_start(va,fmt);
    vfprintf(stderr,fmt,va);
    va_end(va);
  }
  exit(PROG_EXIT_FATAL);
}

/******************************************************************************
 * brlapi handling
 */


#ifndef MIN
#define MIN(a, b) (((a) < (b))? (a): (b))
#endif /* MIN */

static void clipboardContentChanged(brlapi_param_t parameter, brlapi_param_subparam_t subparam, brlapi_param_flags_t flags, void *priv, const void *data, size_t len);

static int tobrltty_init(char *auth, char *host) {
  brlapi_connectionSettings_t settings;
  unsigned int x,y;
  settings.host=host;
  settings.auth=auth;
  static int had_succeeded;
  brlapi_param_clientPriority_t priority;
  brlapi_param_retainDots_t dots;

  if ((brlapi_fd = brlapi_openConnection(&settings,&settings))<0)
  {
    if (!had_succeeded)
    {
      /* This is the first attempt to connect to BRLTTY, and it failed.
       * Return the error immediately to the user, to provide feedback to users
       * running xbrlapi by hand, but not fill logs, eat battery, spam
       * 127.0.0.1 with reconnection attempts.
       */
      fatal_brlapi_errno("openConnection",gettext("cannot connect to braille devices daemon brltty at %s\n"),settings.host);
      exit(PROG_EXIT_FATAL);
    }
    return 0;
  }
  /* We achieved connecting to BRLTTY.  If BRLTTY dies later on, we will
   * silently try to reconnect to it.  */
  had_succeeded = 1;

  if (brlapi_getDisplaySize(&x,&y)<0)
  {
    fatal_brlapi_errno("getDisplaySize",NULL);
    return 0;
  }

  if (x == 0)
  {
    /* Braille device not initialized yet */
    api_cleanExit();
    return 0;
  }

  brlapi_setExceptionHandler(exception_handler);

  /* Our output is really not very interesting */
  priority = 10;
  brlapi_setParameter(BRLAPI_PARAM_CLIENT_PRIORITY, 0, BRLAPI_PARAMF_LOCAL, &priority, sizeof(priority));

  /* We prefer to get translated keypresses */
  dots = 0;
  brlapi_setParameter(BRLAPI_PARAM_RETAIN_DOTS, 0, BRLAPI_PARAMF_LOCAL, &dots, sizeof(dots));

  /* X already has some clipboard content */
  if (clipboardData)
    brlapi_setParameter(BRLAPI_PARAM_CLIPBOARD_CONTENT, 0, BRLAPI_PARAMF_GLOBAL, clipboardData, strlen(clipboardData));

  /* We want to monitor clipboard changes */
  brlapi_watchParameter(BRLAPI_PARAM_CLIPBOARD_CONTENT, 0, BRLAPI_PARAMF_GLOBAL, clipboardContentChanged, NULL, NULL, 0);

  return 1;
}

static int getXVTnb(void);

static void getVT(void) {
  char *path = getenv("WINDOWPATH");
  char *vtnr = getenv("XDG_VTNR");
  int vtno = -1;
  if (!path && !vtnr)
    /* Workaround for old xinit/xdm/gdm/kdm */
    vtno = getXVTnb();

  if (path || vtnr || vtno == -1) {
    if (brlapi_enterTtyModeWithPath(NULL,0,NULL)<0)
    {
      fatal_brlapi_errno("geTtyPath",gettext("cannot get tty\n"));
      return;
    }
  } else {
    if (brlapi_enterTtyMode(vtno,NULL)<0)
    {
      fatal_brlapi_errno("enterTtyMode",gettext("cannot get tty %d\n"),vtno);
      return;
    }
  }

  if (brlapi_ignoreAllKeys()<0)
  {
    fatal_brlapi_errno("ignoreAllKeys",gettext("cannot ignore keys\n"));
    return;
  }
#ifdef CAN_SIMULATE_KEY_PRESSES
  /* All X keysyms with any modifier */
  brlapi_keyCode_t cmd = BRLAPI_KEY_TYPE_SYM;
  if (brlapi_acceptKeys(brlapi_rangeType_type, &cmd, 1))
  {
    fatal_brlapi_errno("acceptKeys",NULL);
    return;
  }
  cmd = BRLAPI_KEY_TYPE_CMD | BRLAPI_KEY_CMD_SHIFT;
  if (brlapi_acceptKeys(brlapi_rangeType_key, &cmd, 1))
  {
    fatal_brlapi_errno("acceptKeys",NULL);
    return;
  }
  cmd = BRLAPI_KEY_TYPE_CMD | BRLAPI_KEY_CMD_UPPER;
  if (brlapi_acceptKeys(brlapi_rangeType_key, &cmd, 1))
  {
    fatal_brlapi_errno("acceptKeys",NULL);
    return;
  }
  cmd = BRLAPI_KEY_TYPE_CMD | BRLAPI_KEY_CMD_CONTROL;
  if (brlapi_acceptKeys(brlapi_rangeType_key, &cmd, 1))
  {
    fatal_brlapi_errno("acceptKeys",NULL);
    return;
  }
  cmd = BRLAPI_KEY_TYPE_CMD | BRLAPI_KEY_CMD_META;
  if (brlapi_acceptKeys(brlapi_rangeType_key, &cmd, 1))
  {
    fatal_brlapi_errno("acceptKeys",NULL);
    return;
  }
#endif /* CAN_SIMULATE_KEY_PRESSES */
}

static char *last_name;

static void api_setLastName(void) {
  if (!last_name) return;
  if (brlapi_writeText(0,last_name)<0) {
    brlapi_perror("writeText");
    fprintf(stderr,gettext("xbrlapi: cannot write window name %s\n"),last_name);
  }
}

static void api_setName(const char *wm_name) {
  if (brlapi_fd<0) return;

  debugf("%s got focus\n",wm_name);
  if (last_name) {
    if (!strcmp(wm_name,last_name)) return;
    free(last_name);
  }
  if (!(last_name=strdup(wm_name))) fatal_errno("strdup(wm_name)",NULL);
  api_setLastName();
}

static int last_win;

static void api_setLastFocus(void)
{
  if (brlapi_setFocus(last_win)<0)
    fatal_brlapi_errno("setFocus",gettext("cannot set focus to %#010x\n"),last_win);
}

static void api_setFocus(int win) {
  if (brlapi_fd<0) return;
  debugf("%#010x (%d) got focus\n",win,win);
  last_win = win;
  api_setLastFocus();
}

/******************************************************************************
 * X handling
 */

static const char *Xdisplay;
static Display *dpy;

static Window curWindow;
static Atom netWmNameAtom, utf8StringAtom;

static XSelData xselData;

static volatile sig_atomic_t grabFailed;

#ifdef HAVE_ICONV_H
iconv_t utf8Conv = (iconv_t)(-1);
#endif /* HAVE_ICONV_H */

#define WINHASHBITS 12

static struct window {
  Window win;
  Window root;
  char *wm_name;
  struct window *next;
} *windows[(1<<WINHASHBITS)];

#define WINHASH(win) windows[(win)>>(32-WINHASHBITS)^(win&((1<<WINHASHBITS)-1))]

static void add_window(Window win, Window root, char *wm_name) {
  struct window *cur;
  if (!(cur=malloc(sizeof(struct window))))
    fatal_errno("malloc(struct window)",NULL);
  cur->win=win;
  cur->wm_name=wm_name;
  cur->root=root;
  cur->next=WINHASH(win);
  WINHASH(win)=cur;
}

static struct window *window_of_Window(Window win) {
  struct window *cur;
  for (cur=WINHASH(win); cur && cur->win!=win; cur=cur->next);
  return cur;
}

static int isRootWindow (Window win) {
  if (win == PointerRoot) return 1;

  {
    int count = ScreenCount(dpy);
     
    for (int index=0; index<count; index+=1) {
      if (RootWindow(dpy, index) == win) {
        return 1;
      }
    }
  }

  return 0;
}

static int del_window(Window win) {
  struct window **pred;
  struct window *cur;

  for (pred=&WINHASH(win); cur = *pred, cur && cur->win!=win; pred=&cur->next);

  if (cur) {
    *pred=cur->next;
    free(cur->wm_name);
    free(cur);
    return 0;
  } else return -1;
}

static int ErrorHandler(Display *dpy, XErrorEvent *ev) {
  char buffer[128];
  if (ev->error_code==BadWindow) {
    grabFailed=1;
    return 0;
  }
#ifdef CAN_SIMULATE_KEY_PRESSES
  if (ev->request_code == xkb_major_opcode && ev->minor_code == X_kbSetMap) {
    /* Server refused our Xkb remapping request, probably the buggy version 21, ignore error */
    fprintf(stderr,gettext("xbrlapi: server refused our mapping request, could not synthesize key\n"));
    return 0;
  }
#endif
  if (XGetErrorText(dpy, ev->error_code, buffer, sizeof(buffer)))
    fatal("XGetErrorText");
  fprintf(stderr,gettext("xbrlapi: X Error %d, %s on display %s\n"), ev->type, buffer, XDisplayName(Xdisplay));
  fprintf(stderr,gettext("xbrlapi: resource %#010lx, req %u:%u\n"),ev->resourceid,ev->request_code,ev->minor_code);
  exit(PROG_EXIT_FATAL);
}

static int getXVTnb(void) {
  Window root;
  Atom property;
  Atom actual_type;
  int actual_format;
  unsigned long nitems;
  unsigned long bytes_after;
  unsigned char *buf;
  int vt = -1;

  root=DefaultRootWindow(dpy);

  if ((property=XInternAtom(dpy,"XFree86_VT",False))==None) {
    fprintf(stderr,gettext("xbrlapi: no XFree86_VT atom\n"));
    return -1;
  }

  if (XGetWindowProperty(dpy,root,property,0,1,False,AnyPropertyType,
    &actual_type, &actual_format, &nitems, &bytes_after, &buf)) {
    fprintf(stderr,gettext("xbrlapi: cannot get root window XFree86_VT property\n"));
    return -1;
  }

  if (nitems<1) {
    fprintf(stderr, gettext("xbrlapi: no items for VT number\n"));
    goto out;
  }
  if (nitems>1)
    fprintf(stderr,gettext("xbrlapi: more than one item for VT number\n"));
  switch (actual_type) {
  case XA_CARDINAL:
  case XA_INTEGER:
  case XA_WINDOW:
    switch (actual_format) {
    case 8:  vt = (*(uint8_t *)buf); break;
    case 16: vt = (*(uint16_t *)buf); break;
    case 32: vt = (*(uint32_t *)buf); break;
    default: fprintf(stderr, gettext("xbrlapi: bad format for VT number\n")); goto out;
    }
    break;
  default: fprintf(stderr, gettext("xbrlapi: bad type for VT number\n")); goto out;
  }
out:
  if (!XFree(buf)) fatal("XFree(VTnobuf)");
  return vt;
}

static int grabWindow(Window win,int level) {
#ifdef DEBUG
  char spaces[level+1];
#endif /* DEBUG */

  grabFailed=0;
  if (!XSelectInput(dpy,win,PropertyChangeMask|FocusChangeMask|SubstructureNotifyMask) || grabFailed)
    return 0;

#ifdef DEBUG
  memset(spaces,' ',level);
  spaces[level]='\0';
  debugf("%sgrabbed %#010lx\n",spaces,win);
#endif /* DEBUG */
  return 1;
}

static char *getWindowTitle(Window win) {
  int wm_name_size=32;
  Atom actual_type;
  int actual_format;
  unsigned long nitems,bytes_after;
  unsigned char *wm_name=NULL;
  char *ret;

  do {
    if (XGetWindowProperty(dpy,win,netWmNameAtom,0,wm_name_size,False,
	/*XA_STRING*/AnyPropertyType,&actual_type,&actual_format,&nitems,&bytes_after,
	&wm_name)) {
      wm_name = NULL;
      break; /* window disappeared or not available */
    }
    wm_name_size+=bytes_after;
    if (!bytes_after) break;
    if (!XFree(wm_name)) fatal("tempo_XFree(wm_name)");
  } while (1);
  if (!wm_name) do {
    if (XGetWindowProperty(dpy,win,XA_WM_NAME,0,wm_name_size,False,
	/*XA_STRING*/AnyPropertyType,&actual_type,&actual_format,&nitems,&bytes_after,
	&wm_name))
      return NULL; /* window disappeared */
    if (wm_name_size >= nitems + 1) break;
    wm_name_size += bytes_after + 1;
    if (!XFree(wm_name)) fatal("tempo_XFree(wm_name)");
  } while (1);
  if (actual_type==None) {
    XFree(wm_name);
    return NULL;
  }
  wm_name[nitems++] = 0;
  ret = strdup((char *) wm_name);
  XFree(wm_name);
  debugf("type %ld name %s len %ld\n",actual_type,ret,nitems);
#ifdef HAVE_ICONV_H
  {
    if (actual_type == utf8StringAtom && utf8Conv != (iconv_t)(-1)) {
      char *ret2;
      size_t input_size, output_size;
      char *input, *output;

      input_size = nitems;
      input = ret;
      output_size = nitems * MB_CUR_MAX;
      output = ret2 = malloc(output_size);
      if (iconv(utf8Conv, &input, &input_size, &output, &output_size) == -1) {
	free(ret2);
      } else {
	free(ret);
	ret = realloc(ret2, nitems * MB_CUR_MAX - output_size);
	debugf("-> %s\n",ret);
      }
    }
  }
#endif /* HAVE_ICONV_H */
  return ret;
}

static int grabWindows(Window win,int level) {
  Window root,parent,*children;
  unsigned int nchildren,i;
  int res=1;

  if (!grabWindow(win,level)) return 1; /* window disappeared */

  if (!XQueryTree(dpy,win,&root,&parent,&children,&nchildren)) return 0;

  add_window(win,root,getWindowTitle(win));

  if (!children) return 1;

  for (i=0;i<nchildren;i++)
    if (children[i] && !grabWindows(children[i],level+1)) {
      res=0;
      break;
    }

  if (!XFree(children)) fatal("XFree(children)");
  return res;
}

static void setName(const struct window *window) {
  if (!window->wm_name) {
    if (window->win!=window->root)
      api_setName("window without name");
  } else api_setName(window->wm_name);
}

static void setFocus(Window win) {
  curWindow=win;
  api_setFocus((uint32_t)win);

  if (!quiet) {
    struct window *window = window_of_Window(win);

    if (window) {
      setName(window);
    } else {
      fprintf(stderr, gettext("xbrlapi: didn't grab window %#010lx but got focus\n"), win);
      api_setName(isRootWindow(win)? "root window": "unnamed window");
    }
  }
}

#ifdef CAN_SIMULATE_KEY_PRESSES
static int tryModifiers(KeyCode keycode, unsigned int *modifiers, unsigned int modifiers_try, KeySym keysym) {
  KeySym keysymRet;
  unsigned int modifiersRet;
  if (!XkbLookupKeySym(dpy, keycode, modifiers_try, &modifiersRet, &keysymRet))
    return 0;
  if (keysymRet != keysym)
    return 0;
  *modifiers |= modifiers_try;
  return 1;
}

static void ignoreServerKeys(void) {
  brlapi_range_t range = {
    .first = BRLAPI_KEY_FLG(ControlMask|Mod1Mask),
    .last  = BRLAPI_KEY_FLG(ControlMask|Mod1Mask)|~BRLAPI_KEY_FLAGS_MASK,
  };
  if (brlapi_ignoreKeyRanges(&range, 1))
  {
    fatal_brlapi_errno("ignoreKeyRanges",NULL);
    return;
  }
}
#endif /* CAN_SIMULATE_KEY_PRESSES */

static void clipboardContentChanged(brlapi_param_t parameter, brlapi_param_subparam_t subparam, brlapi_param_flags_t flags, void *priv, const void *data, size_t len) {
  free(clipboardData);
  clipboardData = strndup(data, len);
  debugf("new clipboard content from BrlAPI: '%s'\n", (const char *) clipboardData);
  if (dpy)
    XSelSet(dpy, &xselData);
}

static void XClipboardContentChanged(const char *data, unsigned long size) {
  free(clipboardData);
  if (data) {
    clipboardData = strndup(data, size);
    brlapi_setParameter(BRLAPI_PARAM_CLIPBOARD_CONTENT, 0, BRLAPI_PARAMF_GLOBAL, clipboardData, size);
    debugf("new clipboard content from X: '%s'\n", (const char *) clipboardData);
  } else
    clipboardData = NULL;
}


static void toX_f(const char *display) {
  Window root;
  XEvent ev;
  int i;
  int X_fd;
  fd_set readfds;
  int maxfd;
#ifdef CAN_SIMULATE_KEY_PRESSES
  int res;
  brlapi_keyCode_t code;
  unsigned int keysym, keycode, modifiers, next_modifiers = 0;
  Bool haveXTest;
  int eventBase, errorBase, majorVersion, minorVersion;
  XkbDescPtr xkb = NULL;
  XkbMapChangesRec changes = { .changed = XkbKeyTypesMask|XkbKeySymsMask };
  int oneGroupType[XkbNumKbdGroups] = { XkbOneLevelIndex };
  Status status;
  int last_remap_keycode = -1, remap_keycode;
#endif /* CAN_SIMULATE_KEY_PRESSES */

  Xdisplay = display;
  if (!Xdisplay) Xdisplay=getenv("DISPLAY");
  if (!(dpy=XOpenDisplay(Xdisplay))) fatal(gettext("cannot connect to display %s\n"),Xdisplay);

  if (!XSetErrorHandler(ErrorHandler)) fatal(gettext("strange old error handler\n"));

#ifdef CAN_SIMULATE_KEY_PRESSES
  haveXTest = XTestQueryExtension(dpy, &eventBase, &errorBase, &majorVersion, &minorVersion);

  {
    int foo;
    int major = XkbMajorVersion, minor = XkbMinorVersion;
    if (!XkbLibraryVersion(&major, &minor))
      fatal(gettext("Incompatible XKB library\n"));
    if (!XkbQueryExtension(dpy, &foo, &foo, &foo, &major, &minor))
      fatal(gettext("Incompatible XKB server support\n"));
    if (!XQueryExtension(dpy, "XKEYBOARD", &xkb_major_opcode, &foo, &foo))
      fatal(gettext("Could not get XKB major opcode\n"));
  }
#endif /* CAN_SIMULATE_KEY_PRESSES */

  XSelInit(dpy, &xselData);

  if (clipboardData)
    XSelSet(dpy, &xselData);

  X_fd = XConnectionNumber(dpy);

  if (brlapi_fd>=0)
  {
    getVT();
#ifdef CAN_SIMULATE_KEY_PRESSES
    ignoreServerKeys();
#endif /* CAN_SIMULATE_KEY_PRESSES */
  }
  netWmNameAtom = XInternAtom(dpy,"_NET_WM_NAME",False);
  utf8StringAtom = XInternAtom(dpy,"UTF8_STRING",False);

#if defined(HAVE_NL_LANGINFO) && defined(HAVE_ICONV_H)
  {
    char *localCharset = nl_langinfo(CODESET);
    if (strcmp(localCharset, "UTF-8")) {
      char buf[strlen(localCharset) + 10 + 1];
      snprintf(buf, sizeof(buf), "%s//TRANSLIT", localCharset);
      if ((utf8Conv = iconv_open(buf, "UTF-8")) == (iconv_t)(-1))
        utf8Conv = iconv_open(localCharset, "UTF-8");
    }
  }
#endif /* defined(HAVE_NL_LANGINFO) && defined(HAVE_ICONV_H) */

  for (i=0;i<ScreenCount(dpy);i++) {
    root=RootWindow(dpy,i);
    if (!grabWindows(root,0)) fatal(gettext("cannot grab windows on screen %d\n"),i);
  }

  {
    Window win;
    int revert_to;
    if (!XGetInputFocus(dpy,&win,&revert_to))
      fatal(gettext("failed to get first focus\n"));
    setFocus(win);
  }
  while(1) {
    struct timeval timeout={.tv_sec=1,.tv_usec=0};
    XFlush(dpy);
    FD_ZERO(&readfds);
    if (brlapi_fd>=0)
      FD_SET(brlapi_fd, &readfds);
    FD_SET(X_fd, &readfds);
    maxfd = brlapi_fd>=0 && X_fd<brlapi_fd ? brlapi_fd+1 : X_fd+1;
    /* Try to reconnect to brlapi every second while disconnected */
    if (select(maxfd,&readfds,NULL,NULL,brlapi_fd<=0?&timeout:NULL)<0)
      fatal_errno("select",NULL);
    if (FD_ISSET(X_fd,&readfds))

    while (XPending(dpy)) {
      if ((i=XNextEvent(dpy,&ev)))
	fatal("XNextEvent: %d\n",i);

      if (!XSelProcess(dpy, &xselData, &ev, clipboardData, XClipboardContentChanged))
      switch (ev.type) {
      /* focus events */
      case FocusIn:
	switch (ev.xfocus.detail) {
	case NotifyAncestor:
	case NotifyInferior:
	case NotifyNonlinear:
	case NotifyPointerRoot:
	case NotifyDetailNone:
	  setFocus(ev.xfocus.window); break;
	} break;
      case FocusOut:
	/* ignore
	switch (ev.xfocus.detail) {
	case NotifyAncestor:
	case NotifyInferior:
	case NotifyNonlinear:
	case NotifyPointerRoot:
	case NotifyDetailNone:
	printf("win %#010lx lost focus\n",ev.xfocus.window);
	break;
	}
	*/
	break;

      /* create & destroy events */
      case CreateNotify: {
      /* there's a race condition here : a window may get the focus or change
       * its title between it is created and we achieve XSelectInput on it */
	Window win = ev.xcreatewindow.window;
	struct window *window;
	if (!grabWindow(win,0)) break; /* window already disappeared ! */
	debugf("win %#010lx created\n",win);
	if (!(window = window_of_Window(ev.xcreatewindow.parent))) {
	  fprintf(stderr,gettext("xbrlapi: didn't grab parent of %#010lx\n"),win);
	  add_window(win,None,getWindowTitle(win));
	} else add_window(win,window->root,getWindowTitle(win));
      } break;
      case DestroyNotify:
	debugf("win %#010lx destroyed\n",ev.xdestroywindow.window);
	if (del_window(ev.xdestroywindow.window))
	  debugf("destroy: didn't grab window %#010lx\n",ev.xdestroywindow.window);
	break;

      /* Property change: WM_NAME ? */
      case PropertyNotify:
	if (ev.xproperty.atom==XA_WM_NAME ||
	    (netWmNameAtom != None && ev.xproperty.atom == netWmNameAtom)) {
	  Window win = ev.xproperty.window;
	  debugf("WM_NAME property of %#010lx changed\n",win);
	  struct window *window;
	  if (!(window=window_of_Window(win))) {
	    fprintf(stderr,gettext("xbrlapi: didn't grab window %#010lx\n"),win);
	    add_window(win,None,getWindowTitle(win));
	  } else {
	    if (window->wm_name)
	      if (!XFree(window->wm_name)) fatal(gettext("XFree(wm_name) for change"));
	    if ((window->wm_name=getWindowTitle(win))) {
	      if (!quiet && win==curWindow)
		api_setName(window->wm_name);
	    } else fprintf(stderr,gettext("xbrlapi: window %#010lx changed to NULL name\n"),win);
	  }
	}
	break;
      case MappingNotify:
	XRefreshKeyboardMapping(&ev.xmapping);
	break;
      /* ignored events */
      case UnmapNotify:
      case MapNotify:
      case MapRequest:
      case ReparentNotify:
      case ConfigureNotify:
      case GravityNotify:
      case ConfigureRequest:
      case CirculateNotify:
      case CirculateRequest:
      case ClientMessage:
	break;

      /* "shouldn't happen" events */
      default: fprintf(stderr,gettext("xbrlapi: unhandled event type: %d\n"),ev.type); break;
      }
    }
    if (brlapi_fd>=0) {
#ifdef CAN_SIMULATE_KEY_PRESSES
     if (haveXTest && FD_ISSET(brlapi_fd,&readfds)) {
      while (((res = brlapi_readKey(0, &code))==1)) {
	switch (code & BRLAPI_KEY_TYPE_MASK) {
	  case BRLAPI_KEY_TYPE_CMD:
	    switch (code & BRLAPI_KEY_CODE_MASK) {
              {
                unsigned int modifier;

              case BRLAPI_KEY_CMD_SHIFT:
                modifier = ShiftMask;
                goto doModifier;

              case BRLAPI_KEY_CMD_UPPER:
                modifier = LockMask;
                goto doModifier;

              case BRLAPI_KEY_CMD_CONTROL:
                modifier = ControlMask;
                goto doModifier;

              case BRLAPI_KEY_CMD_META:
                modifier = Mod1Mask;
                goto doModifier;

              doModifier:
                switch (code & BRLAPI_KEY_FLG_TOGGLE_MASK) {
                  case 0:
                    next_modifiers ^= modifier;
                    break;

                  case BRLAPI_KEY_FLG_TOGGLE_ON:
                    next_modifiers |= modifier;
                    break;

                  case BRLAPI_KEY_FLG_TOGGLE_OFF:
                    next_modifiers &= ~modifier;
                    break;

                  default:
                  case BRLAPI_KEY_FLG_TOGGLE_ON | BRLAPI_KEY_FLG_TOGGLE_OFF:
                    break;
                }

                break;
              }

	      default:
		fprintf(stderr, "xbrlapi: %s: %" BRLAPI_PRIxKEYCODE "\n",
			gettext("unexpected cmd"), code);
		break;
	    }
	    break;
	  case BRLAPI_KEY_TYPE_SYM:
	    modifiers = ((code & BRLAPI_KEY_FLAGS_MASK) >> BRLAPI_KEY_FLAGS_SHIFT) & 0xFF;
	    keysym = code & BRLAPI_KEY_CODE_MASK;
	    keycode = XKeysymToKeycode(dpy,keysym);
	    remap_keycode = -1;
	    if (keycode == NoSymbol) {
	      debugf(gettext("xbrlapi: Couldn't translate keysym %08X to keycode.\n"),keysym);
	      goto needRemap;
	    }

	    {
	      static const unsigned int tryTable[] = {
		0,
		ShiftMask,
		Mod2Mask,
		Mod3Mask,
		Mod4Mask,
		Mod5Mask,
		ShiftMask|Mod2Mask,
		ShiftMask|Mod3Mask,
		ShiftMask|Mod4Mask,
		ShiftMask|Mod5Mask,
		0
	      };
	      const unsigned int *try = tryTable;

	      do {
		if (tryModifiers(keycode, &modifiers, *try, keysym)) goto foundModifiers;
	      } while (*++try);

	      debugf(gettext("xbrlapi: Couldn't find modifiers to apply to %d for getting keysym %08X\n"),keycode,keysym);
	    }

	  needRemap:
	    {
	      /* Try tofind an unassigned keycode to remap it temporarily to the requested keysym. */
	      xkb = XkbGetMap(dpy,XkbKeyTypesMask|XkbKeySymsMask,XkbUseCoreKbd);
	      /* Start from big keycodes, usually unassigned. */
	      for (i = xkb->max_key_code;
		   i >= xkb->min_key_code && (XkbKeyNumGroups(xkb,i) != 0 || i == last_remap_keycode);
		   i--)
		;
	      if (i < xkb->min_key_code) {
		fprintf(stderr,gettext("xbrlapi: Couldn't find a keycode to remap for simulating unbound keysym %08X\n"),keysym);
		goto abortRemap;
	      }
	      remap_keycode = keycode = i;
	      next_modifiers = modifiers = 0;

	      /* Remap this keycode. */
	      changes.first_key_sym = keycode;
	      changes.num_key_syms = 1;
	      if ((status = XkbChangeTypesOfKey(xkb,keycode,1,XkbGroup1Mask,oneGroupType,&changes))) {
		debugf("Error while changing client keymap: %d\n", status);
		goto abortRemap;
	      }
	      XkbKeySymEntry(xkb,keycode,0,0) = keysym;
	      if (!XkbChangeMap(dpy,xkb,&changes)) {
		debugf("Error while changing server keymap\n");
		goto abortRemap;
	      }
	      XkbFreeKeyboard(xkb,0,True);
	      debugf("Remapped keycode %d to keysym %08X\n",keycode,keysym);
	    }

	  foundModifiers:
	    debugf("key %08X: (%d,%x,%x)\n", keysym, keycode, next_modifiers, modifiers);
	    modifiers |= next_modifiers;
	    next_modifiers = 0;
	    if (modifiers)
	      XkbLockModifiers(dpy, XkbUseCoreKbd, modifiers, modifiers);
	    XTestFakeKeyEvent(dpy,keycode,True,1);
	    XTestFakeKeyEvent(dpy,keycode,False,1);
	    if (modifiers)
	      XkbLockModifiers(dpy, XkbUseCoreKbd, modifiers, 0);

	    /* Remove previous keycode mapping */
	    if (last_remap_keycode != -1) {
	      /* Note: since X11 is asynchronous, we should not immediately
	       * unmap the just-mapped keycode, otherwise when the client
	       * eventually gets to read the new Xkb state from the server,
	       * the key might have been synthesized and the keycode unmapped
	       * already. We just hope the user does not type too fast for the
	       * application to catch up. */
	      xkb = XkbGetMap(dpy,XkbKeyTypesMask|XkbKeySymsMask,XkbUseCoreKbd);
	      changes.first_key_sym = last_remap_keycode;
	      changes.num_key_syms = 1;
	      if ((status = XkbChangeTypesOfKey(xkb,last_remap_keycode,0,XkbGroup1Mask,NULL,&changes))) {
		debugf("Oops, error while restoring client keymap: %d\n", status);
	      } else {
		XkbChangeMap(dpy,xkb,&changes);
		debugf("restored last keycode %d\n", last_remap_keycode);
	      }
	      XkbFreeKeyboard(xkb,0,True);
	    }
	    XFlush(dpy);
	    last_remap_keycode = remap_keycode;
	    break;

	  abortRemap:
	    XkbFreeKeyboard(xkb, 0, True);
	    xkb = NULL;
	    break;

	  default:
	    fprintf(stderr, "xbrlapi: %s: %" BRLAPI_PRIxKEYCODE "\n",
		    gettext("unexpected block type"), code);
	    next_modifiers = 0;
	    break;
	}
      }
      if (res<0)
	fatal_brlapi_errno("brlapi_readKey",NULL);
     }
#endif /* CAN_SIMULATE_KEY_PRESSES */
    } else {
      /* Try to reconnect */
      if (tobrltty_init(auth,host))
      {
	getVT();
#ifdef CAN_SIMULATE_KEY_PRESSES
	ignoreServerKeys();
#endif /* CAN_SIMULATE_KEY_PRESSES */
	api_setLastName();
	api_setLastFocus();
      }
    }
  }
}

/******************************************************************************
 * main
 */

static void term_handler(int foo) {
  api_cleanExit();
  exit(PROG_EXIT_SUCCESS);
}

int
main (int argc, char *argv[]) {
  {
    static const OptionsDescriptor descriptor = {
      OPTION_TABLE(programOptions),
      .applicationName = "xbrlapi"
    };
    PROCESS_OPTIONS(descriptor, argc, argv);
  }

  signal(SIGTERM,term_handler);
  signal(SIGINT,term_handler);
#ifdef SIGHUP
  signal(SIGHUP,term_handler);
#endif /* SIGHUP */
#ifdef SIGQUIT
  signal(SIGQUIT,term_handler);
#endif /* SIGQUIT */
#ifdef SIGPIPE
  signal(SIGPIPE,term_handler);
#endif /* SIGPIPE */

  tobrltty_init(auth,host);

  if (!no_daemon) {
    pid_t child = fork();
    if (child == -1)
      fatal_errno("failed to fork", NULL);

    if (child)
      exit(PROG_EXIT_SUCCESS);

    if (setsid() == -1)
      fatal_errno("failed to create background session", NULL);
  }

  toX_f(xDisplay);

  return PROG_EXIT_SUCCESS;
}
