https://bugs.gentoo.org/966686
https://github.com/jordansissel/xdotool/issues/491
https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config/-/issues/554

Rebased version of a not-yet-merged fix[1] (excludes tests) that, as
of the writing of this, is still undergoing testing upstream and may
or may not work as expected (hoping for a new release soon).

[1] https://github.com/jordansissel/xdotool/pull/493
--- a/xdo.c
+++ b/xdo.c
@@ -48,2 +48,5 @@
 
+static const char *vmodnames(Display *dpy, XkbDescPtr desc, short vmods);
+static const char *modnames(short mask);
+
 static void _xdo_populate_charcode_map(xdo_t *xdo);
@@ -66,3 +69,2 @@
 
-static int _xdo_query_keycode_to_modifier(XModifierKeymap *modmap, KeyCode keycode);
 static int _xdo_mousebutton(const xdo_t *xdo, Window window, int button, int is_press);
@@ -144,2 +146,6 @@
 
+  if (getenv("DEBUG")) {
+    xdo->debug = True;
+  }
+
   if (_xdo_has_xtest(xdo)) {
@@ -1090,3 +1096,4 @@
       KeySym keysym_list[] = { keys[i].symbol };
-      _xdo_debug(xdo, "Mapping sym %lu to %d", keys[i].symbol, scratch_keycode);
+      const char *text = XKeysymToString(keys[i].symbol);
+      _xdo_debug(xdo, "Mapping sym %lu (%s) to %d", keys[i].symbol, text, scratch_keycode);
       XChangeKeyboardMapping(xdo->xdpy, scratch_keycode, 1, keysym_list, 1);
@@ -1283,2 +1290,3 @@
   _xdo_charcodemap_from_keysym(xdo, key, keysym);
+  _xdo_debug(xdo, "Char '%c' made with Symbol(%s) using keycode %d mask %s", key->key, XKeysymToString(keysym), key->code, modnames(key->modmask));
 
@@ -1312,2 +1320,3 @@
   }
+  _xdo_debug(xdo, "No mapping found: Symbol(%s)", XKeysymToString(keysym));
 }
@@ -1319,46 +1328,143 @@
 
-static void _xdo_populate_charcode_map(xdo_t *xdo) {
-  /* assert xdo->display is valid */
-  int keycodes_length = 0;
-  int idx = 0;
-  int keycode, group, groups, level, modmask, num_map;
-
-  XDisplayKeycodes(xdo->xdpy, &(xdo->keycode_low), &(xdo->keycode_high));
-  XModifierKeymap *modmap = XGetModifierMapping(xdo->xdpy);
-  KeySym *keysyms = XGetKeyboardMapping(xdo->xdpy, xdo->keycode_low,
-                                        xdo->keycode_high - xdo->keycode_low + 1,
-                                        &xdo->keysyms_per_keycode);
-  XFree(keysyms);
+#define AddCharcodeEntry(idx, xdo, keysym, keycode, group, mask) \
+          if (idx == charcodes_size) { \
+            xdo->charcodes = realloc(xdo->charcodes, (charcodes_size += 100) * sizeof(charcodemap_t)); \
+          } \
+          xdo->charcodes[idx].key = _keysym_to_char(keysym); \
+          xdo->charcodes[idx].code = keycode; \
+          xdo->charcodes[idx].group = group; \
+          xdo->charcodes[idx].modmask = mask; \
+          xdo->charcodes[idx].symbol = keysym; \
+          xdo->charcodes_len = idx; \
+          idx++; 
 
-  /* Add 2 to the size because the range [low, high] is inclusive */
-  /* Add 2 more for tab (\t) and newline (\n) */
-  keycodes_length = ((xdo->keycode_high - xdo->keycode_low) + 1)
-                     * xdo->keysyms_per_keycode;
 
-  xdo->charcodes = calloc(keycodes_length, sizeof(charcodemap_t));
+static void _xdo_populate_charcode_map(xdo_t *xdo) {
+  size_t idx = 0;
   XkbDescPtr desc = XkbGetMap(xdo->xdpy, XkbAllClientInfoMask, XkbUseCoreKbd);
 
-  for (keycode = xdo->keycode_low; keycode <= xdo->keycode_high; keycode++) {
-    groups = XkbKeyNumGroups(desc, keycode);
-    for (group = 0; group < groups; group++) {
+  xdo->keycode_low = desc->min_key_code;
+  xdo->keycode_high = desc->max_key_code;
+
+  // Fetch atom names so XGetAtomName works on Xkb atoms
+  XkbGetNames(
+      xdo->xdpy,
+      XkbKeyTypeNamesMask | XkbKTLevelNamesMask | XkbVirtualModNamesMask, desc);
+
+  size_t charcodes_size = 100;
+
+  xdo->charcodes = calloc(charcodes_size, sizeof(charcodemap_t));
+
+  Display* dpy = xdo->xdpy;
+
+  // Scan all known keycodes and modifier mappings to find what symbols can be typed
+  for (int keycode = desc->min_key_code; keycode <= desc->max_key_code; keycode++) {
+
+    // For each keycode, scan across the groups
+    for (int group = 0; group < XkbKeyNumGroups(desc, keycode); group++) {
+
       XkbKeyTypePtr key_type = XkbKeyKeyType(desc, keycode, group);
-      for (level = 0; level < key_type->num_levels; level++) {
-        KeySym keysym = XkbKeycodeToKeysym(xdo->xdpy, keycode, group, level);
-        modmask = 0;
-
-        for (num_map = 0; num_map < key_type->map_count; num_map++) {
-          XkbKTMapEntryRec map = key_type->map[num_map];
-          if (map.active && map.level == level) {
-            modmask = map.mods.mask;
-            break;
+      if (key_type->num_levels == 0) {
+        printf("Bug? No shift levels found for code:%d, group: %d\n", keycode,
+               group);
+        continue;
+      }
+      // For each shift level on this keycode and group
+      for (unsigned char li = 0; li < key_type->num_levels; li++) {
+        XkbKTMapEntryRec map = {
+          .active =  False,
+          .level =  0,
+          .mods = {
+            .mask = 0,
+            .real_mods = 0,
+            .vmods = 0,
           }
+        };
+
+        KeySym keysym = XkbKeycodeToKeysym(dpy, keycode, group, map.level);
+
+        if (keysym == NoSymbol) {
+          // No keysym produced by the given (keycode, group, shift level)
+          _xdo_debug(
+              xdo, "[group %d, level %s[%d]] (KT: %s) keycode %d is not bound at this level",
+              group, XGetAtomName(dpy, key_type->level_names[li]),
+              li + 1 /* levels are named starting at 1 */,
+              XGetAtomName(dpy, key_type->name), keycode);
+          continue;
+        }
+
+        /* From:
+         * https://x.z-yx.cc/libX11/XKB/16-chapter-15-xkb-client-keyboard-mapping.html#The_Canonical_Key_Types
+         * > Any combination of modifiers not explicitly listed somewhere in
+         * > the map yields shift level one.
+         *
+         * Also: xkbcomp will delete all level 1 key map entries when compiling.
+         * Reference:
+         * https://gitlab.freedesktop.org/xorg/app/xkbcomp/-/blob/d03a4ab1c0b24f6581411622ccf729ceb329aeb8/keytypes.c#L1247
+         *
+         * Thus, if no active map entry is found, and the current shift
+         * level is "one" (0), we must assume that the keycode alone will
+         * produce a keysym.
+         *
+         * Further, if no map entry with 'mask == 0' is found, it means that
+         * shift level one is produced when the keycode is used without any
+         * modifiers.
+         *
+         * As a shortcut, for shift level 1 (li == 0), check if the keycode
+         * produces this level's keysym without any modifiers. If so, then
+         * prefer it in the keymap.
+         */
+        if (li == 0) {
+          KeySym key_without_mask;
+          // Ask what keysym is produced by this keycode when mask == 0.
+          if (XkbTranslateKeyCode(desc, keycode, 0 /* modifier mask */, NULL, &key_without_mask) == True) {
+            // Safety check: does the keysym found above match the keysm produced by this shift level?
+            if (key_without_mask == keysym) {
+              // pretend we found the map entry in the XkbKTMap
+              map.level = li;
+              map.active = True;
+            }
+          }
+        }
+
+        // Find out if there's any active modifier mappings for this keycode, group, and shift level.
+        // We can stop looking after we find one.
+        for (int mi = 0; mi < key_type->map_count && !map.active; mi++) {
+          if (key_type->map[mi].active && key_type->map[mi].level == li) {
+            //_xdo_debug(xdo, "Found modifiers -> level mapping for keycode %d level %d", keycode, li);
+            memcpy(&map, &(key_type->map[mi]), sizeof(map));
+
+            if (map.mods.real_mods == 0 && map.mods.vmods == 0) {
+              _xdo_debug(xdo, "Warning: found a mod entry with mods=0. This isn't expected?");
+            }
+            keysym = XkbKeycodeToKeysym(dpy, keycode, group, map.level);
+          }
+        }
+
+        // If no active mapping is found, then no keysym is produced by this
+        // keycode+modifier at this shift level.
+        if (!map.active) {
+            continue;
         }
 
-        xdo->charcodes[idx].key = _keysym_to_char(keysym);
-        xdo->charcodes[idx].code = keycode;
-        xdo->charcodes[idx].group = group;
-        xdo->charcodes[idx].modmask = modmask | _xdo_query_keycode_to_modifier(modmap, keycode);
-        xdo->charcodes[idx].symbol = keysym;
+        _xdo_debug(
+          xdo,
+          "[group %d, level %s[%d]] Symbol(%s) = keycode %d with "
+          "mask:%s, "
+          "real_mods:%x, vmods:%s%s",
+          group, XGetAtomName(dpy, key_type->level_names[li]),
+          li + 1 /* levels are named starting at 1 */,
+          XKeysymToString(keysym), keycode,
+          modnames(map.mods.mask), map.mods.real_mods,
+          vmodnames(dpy, desc, map.mods.vmods),
+
+          // Here, "IMPLICIT" means: Xkb defines any missing mappings as
+          // reaching shift level 1. Per the Xkb documentation: 
+          // > Any combination of modifiers not explicitly listed somewhere in the
+          // > map yields shift level one
+          // If no active mapping was found for level 1 (li == 0), the keycode alone
+          // shall be valid.
+          (!map.active && li == 0) ? " [IMPLICIT] " : "");
 
-        idx++;
+        AddCharcodeEntry(idx, xdo, keysym, keycode, group, map.mods.mask);
       }
@@ -1366,7 +1472,7 @@
   }
-  xdo->charcodes_len = idx;
-  XkbFreeClientMap(desc, 0, 1);
-  XFreeModifiermap(modmap);
+
+  XkbFreeKeyboard(desc, 0, True);
 }
 
+
 /* context-free functions */
@@ -1559,3 +1665,2 @@
   if (use_xtest) {
-    //printf("XTEST: Sending key %d %s\n", key->code, is_press ? "down" : "up");
     XkbStateRec state;
@@ -1566,3 +1671,4 @@
       _xdo_send_modifier(xdo, mask, is_press);
-    //printf("XTEST: Sending key %d %s %x %d\n", key->code, is_press ? "down" : "up", key->modmask, key->group);
+    _xdo_debug(xdo, "XTEST: %s key: keycode %d", is_press ? "Press" : "Release",
+               key->code);
     XTestFakeKeyEvent(xdo->xdpy, key->code, is_press, CurrentTime);
@@ -1571,2 +1677,3 @@
   } else {
+    _xdo_debug(xdo, "XSendEvent: Sending key %s, keycode %d mask %s\n", is_press ? "down" : "up", key->code, modnames(key->modmask));
     /* Since key events have 'state' (shift, etc) in the event, we don't
@@ -1589,27 +1696,2 @@
 
-int _xdo_query_keycode_to_modifier(XModifierKeymap *modmap, KeyCode keycode) {
-  int i = 0, j = 0;
-  int max = modmap->max_keypermod;
-
-  for (i = 0; i < 8; i++) { /* 8 modifier types, per XGetModifierMapping(3X) */
-    for (j = 0; j < max && modmap->modifiermap[(i * max) + j]; j++) {
-      if (keycode == modmap->modifiermap[(i * max) + j]) {
-        switch (i) {
-          case ShiftMapIndex: return ShiftMask; break;
-          case LockMapIndex: return LockMask; break;
-          case ControlMapIndex: return ControlMask; break;
-          case Mod1MapIndex: return Mod1Mask; break;
-          case Mod2MapIndex: return Mod2Mask; break;
-          case Mod3MapIndex: return Mod3Mask; break;
-          case Mod4MapIndex: return Mod4Mask; break;
-          case Mod5MapIndex: return Mod5Mask; break;
-        }
-      } /* end if */
-    } /* end loop j */
-  } /* end loop i */
-
-  /* No modifier found for this keycode, return no mask */
-  return 0;
-}
-
 void _xdo_send_modifier(const xdo_t *xdo, int modmask, int is_press) {
@@ -1623,2 +1705,8 @@
         if (keycode) {
+          _xdo_debug(
+            xdo,
+            "XTEST: %s modifier %s using keycode %d",
+            is_press ? "Press" : "Release",
+            modnames(modmask & (1 << mod_index)),
+            keycode);
           XTestFakeKeyEvent(xdo->xdpy, keycode, is_press, CurrentTime);
@@ -2060,2 +2148,62 @@
   return 0;
+}
+
+static const char *vmodnames(Display *dpy, XkbDescPtr desc, short vmods) {
+  static char names[1024];
+  memset(names, 0, sizeof(names));
+
+  if (vmods == 0) {
+    return "<none>";
+  }
+
+  for (int i = 0; (1 << i) <= vmods; i++) {
+    if (vmods & (1 << i)) {
+      const char *n = XGetAtomName(dpy, desc->names->vmods[i]);
+      if (names[0] == 0) {
+        strncpy(names, n, 20);
+      } else {
+        strncpy(names+strlen(names), "+", 20);
+        strncpy(names+strlen(names), n,20);
+      }
+    }
+  }
+
+  if (strlen(names) == 0) {
+    printf("bug: empty vmod string for value: %d\n", vmods);
+  }
+
+  return names;
+}
+
+static const char *modnames(short mask) {
+  static char names[1024];
+  memset(names, 0, sizeof(names));
+
+  if (mask == 0) {
+    return "<none>";
+  }
+
+  if (mask & ShiftMask)
+    strncat(names, "+Shift", 20);
+  if (mask & LockMask)
+    strncat(names, "+Lock", 20);
+  if (mask & ControlMask)
+    strncat(names, "+Control", 20);
+  if (mask & Mod1Mask)
+    strncat(names, "+Mod1", 20);
+  if (mask & Mod2Mask)
+    strncat(names, "+Mod2", 20);
+  if (mask & Mod3Mask)
+    strncat(names, "+Mod3", 20);
+  if (mask & Mod4Mask)
+    strncat(names, "+Mod4", 20);
+  if (mask & Mod5Mask)
+    strncat(names, "+Mod5", 20);
+
+  if (names[0] == '+') {
+    memmove(names, names+1, strlen(names)-1);
+    names[strlen(names)-1] = 0;
+  }
+
+  return names;
 }
\ No newline at end of file
