/*
 * XBrlAPI - A background process tinkering with X for proper BrlAPI behavior
 *
 * Copyright (C) 2003-2024 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/Xutil.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 "cmdline.h"

#define debugf(fmt, ...) \
do { \
  if (verbose) { \
    FILE *stream = stderr; \
    fprintf(stream, fmt, ## __VA_ARGS__); \
    fflush(stream); \
  } \
} while (0)

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

static char *host;
static char *auth;
static char *xDisplay;
static int no_daemon;
static int quiet;
static int verbose;
static char *commandNameFile;
static char *windowNameFile;

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 = "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")
  },

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

  { .word = "write-command",
    .letter = 'c',
    .argument = strtext("file"),
    .setting.string = &commandNameFile,
    .description = strtext("file to write the name of the focused command to")
  },

  { .word = "write-window",
    .letter = 'w',
    .argument = strtext("file"),
    .setting.string = &windowNameFile,
    .description = strtext("file to write the name of the focused window to")
  },
END_OPTION_TABLE(programOptions)

/******************************************************************************
 * 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;

  if (last_name) {
    if (strcmp(wm_name, last_name) == 0) 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, netWmCommandAtom, utf8StringAtom;

static XSelData xselData;

static volatile sig_atomic_t grabFailed;

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

static int isWindowProperty(Atom property, Atom netProperty, Atom xaProperty) {
  if (netProperty != None) {
    if (property == netProperty) {
      return 1;
    }
  }

  return property == xaProperty;
}

static char *getWindowProperty(Window win, Atom netProperty, Atom xaProperty) {
  const long offset_0 = 0;
  const Bool dont_delete = False;

  char *ret;
  unsigned char *value = NULL;
  long value_length = 32;

  Atom actual_type;
  int actual_format;
  unsigned long item_count;
  unsigned long bytes_after;

  if (netProperty != None) do {
    Status result = XGetWindowProperty(
      dpy, win, netProperty, offset_0, value_length, dont_delete,
      /*XA_STRING*/AnyPropertyType,
      &actual_type, &actual_format, &item_count, &bytes_after,
      &value
    );

    if (result != Success) {
      value = NULL;
      break; /* window disappeared or not available */
    }

    value_length += bytes_after;
    if (!bytes_after) break;
    if (!XFree(value)) fatal("tempo_XFree(value)");
  } while (1);

  if (!value) do {
    Status result = XGetWindowProperty(
      dpy, win, xaProperty, offset_0, value_length, dont_delete,
      /*XA_STRING*/AnyPropertyType,
      &actual_type, &actual_format, &item_count, &bytes_after,
      &value
    );

    if (result != Success) {
      return NULL; /* window disappeared */
    }

    if (value_length >= (item_count + 1)) break;
    value_length += bytes_after + 1;
    if (!XFree(value)) fatal("tempo_XFree(value)");
  } while (1);

  if (actual_type == None) {
    XFree(value);
    return NULL;
  }

  value[item_count++] = 0;
  ret = strdup((char *)value);
  XFree(value);
  debugf("window property: type %ld, len %ld, value \"%s\"\n", actual_type, item_count, ret);

#ifdef HAVE_ICONV_H
  {
    if ((actual_type == utf8StringAtom) && (utf8Conv != (iconv_t)(-1))) {
      size_t input_size = item_count;
      char *input = ret;
      size_t output_size = item_count * MB_CUR_MAX;
      char *output = malloc(output_size);
      char *ret2 = output;

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

  return ret;
}

static char *getWindowName(Window win) {
  return getWindowProperty(win, netWmNameAtom, XA_WM_NAME);
}

static char *getWindowCommand(Window win) {
  return getWindowProperty(win, netWmCommandAtom, XA_WM_COMMAND);
}

static char *getWindowApplicationName(Window win) {
  char *name = NULL;
  XClassHint *hint = XAllocClassHint();

  if (hint) {
    if (XGetClassHint(dpy, win, hint)) {
      if (hint->res_class) XFree(hint->res_class);

      if (hint->res_name) {
        name = strdup(hint->res_name);
        XFree(hint->res_name);
      }
    }

    XFree(hint);
  }

  return name;
}

#define WINHASHBITS 12

static struct window {
  Window win;
  Window root;
  Window parent;

  char *wm_name;
  char *wm_command;

  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, Window parent) {
  struct window *cur;

  if (!(cur = malloc(sizeof(*cur)))) {
    fatal_errno("malloc(struct window)",NULL);
  }

  cur->win = win;
  cur->root = root;
  cur->parent = parent;

  cur->wm_name = getWindowName(win);
  cur->wm_command = getWindowCommand(win);

  cur->next = WINHASH(win);
  WINHASH(win) = cur;

  debugf(
    "window %#010lx added: root %#010lx, parent %#010lx\n",
    win, root, parent
  );
}

static void add_orphan_window(Window win) {
  add_window(win, None, None);
}

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->wm_command);
    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 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,parent);

  if (!children) return 1;

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

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

static void writeStateToFile(const char *file, const char *state) {
  if (*file) {
    FILE *stream = fopen(file, "w");

    if (stream) {
      fputs(state, stream);
      fclose(stream);
    }
  }
}

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

  struct window *window = window_of_Window(win);
  const char *windowName = NULL;
  char *commandName = NULL;

  if (window) {
    windowName = window->wm_name;
    commandName = getWindowApplicationName(win);
  } else {
    fprintf(stderr, gettext("xbrlapi: didn't grab window %#010lx but got focus\n"), win);
    windowName = isRootWindow(win)? "root window": "unknown window";
  }

  if (!windowName) windowName = "";
  if (!*windowName) windowName = "unnamed window";
  debugf("focused window: %s\n", windowName);
  writeStateToFile(windowNameFile, windowName);
  if (!quiet) api_setName(windowName);

  if (!commandName) commandName = strdup("");
  debugf("focused command: %s\n", commandName);
  writeStateToFile(commandNameFile, commandName);
  free(commandName);
}

#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);
  netWmCommandAtom = None;
  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 name between when it's created and we achieve XSelectInput on it */
	Window win = ev.xcreatewindow.window;
	if (!grabWindow(win,0)) break; /* window already disappeared! */
	debugf("win %#010lx created\n",win);

	struct window *parent_window;
	if (!(parent_window = window_of_Window(ev.xcreatewindow.parent))) {
	  fprintf(stderr,gettext("xbrlapi: didn't grab parent of %#010lx\n"),win);
	  add_orphan_window(win);
	} else {
          add_window(win, parent_window->root, parent_window->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: {
        Window win = ev.xproperty.window;
        struct window *window = window_of_Window(win);

	if (isWindowProperty(ev.xproperty.atom, netWmNameAtom, XA_WM_NAME)) {
	  debugf("WM_NAME property of %#010lx changed\n", win);

	  if (!window) {
	    fprintf(stderr, gettext("xbrlapi: didn't grab window %#010lx\n"), win);
	    add_orphan_window(win);
	  } else {
	    if (window->wm_name) {
	      if (!XFree(window->wm_name)) fatal(gettext("XFree(wm_name) for change"));
            }

	    if ((window->wm_name = getWindowName(win))) {
	      if (!quiet && (win == curWindow)) {
		api_setName(window->wm_name);
              }
	    } else {
              fprintf(stderr,gettext("xbrlapi: window %#010lx changed to NULL name\n"), win);
            }
	  }
	} else if (isWindowProperty(ev.xproperty.atom, netWmCommandAtom, XA_WM_COMMAND)) {
	  debugf("WM_COMMAND property of %#010lx changed\n", win);

	  if (!window) {
	    fprintf(stderr, gettext("xbrlapi: didn't grab window %#010lx\n"), win);
	    add_orphan_window(win);
	  } else {
	    if (window->wm_command) {
	      if (!XFree(window->wm_command)) fatal(gettext("XFree(wm_command) for change"));
            }

	    if ((window->wm_command = getWindowCommand(win))) {
	    } else {
              fprintf(stderr,gettext("xbrlapi: window %#010lx changed to NULL command\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[]) {
  {
    const CommandLineDescriptor descriptor = {
      .options = &programOptions,
      .applicationName = "xbrlapi",

      .usage = {
        .purpose = strtext("Augment an X session by supporting input typed on the braille device, showing the title of the focused window on the braille display, and switching braille focus to it."),
      }
    };

    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;
}
