Package: xscreensaver
Version: 6.09+dfsg1-2
Severity: wishlist
Tags: patch, l10n, upstream
User: [email protected]
Usertags: russian
X-Debbugs-Cc: [email protected], [email protected]

Dear Maintainer,

Unfortunately, I don't know how to contact the xscreensaver upstream 
(couldn't find a public repository for this program), so I'm reporting 
this here. Perhaps the Debian package maintainer can help forward this 
upstream?

I've created a patch that I believe will be very useful for non-English 
speaking users, who often struggle with having the wrong keyboard layout 
active when unlocking their workstation, making it difficult to enter 
their password correctly.

Since xscreensaver is designed to receive scancodes from password input 
keys and then translate them into password text, I simply added an option 
where users can predetermine which layout should be used for password input. 
When this option is set, the scancode-to-password translation becomes 
independent of the current keyboard layout, making xscreensaver significantly 
more comfortable to use for users who switch keyboard layouts.

I've also created a merge request in Debian salsa Git:
https://salsa.debian.org/debian/xscreensaver/-/merge_requests/8

The patch is attached below and can be applied to vanilla xscreensaver 
sources with: patch -p1 < 60_force_keyboard_layout.patch

Best regards,
Dmitry Oboukhov

---BEGIN PATCH---
Description: Add keyboard layout forcing for password entry
 This patch adds a new configuration parameter 'passwdKeyboardGroup'
 that allows users to force a specific keyboard layout when entering
 their password on the unlock dialog.
 .
 This is useful for non-English users who may have the wrong keyboard
 layout active when the screen locks, making it difficult to enter
 their password correctly.
 .
 The parameter accepts values from -1 to 3:
  -1: Disabled (use current keyboard layout)
   0-3: Force specific XKB group (keyboard layout)
 .
 A GUI control is added to the Advanced tab in xscreensaver-settings
 to configure this option.
Author: Dmitry E. Oboukhov <[email protected]>
Forwarded: no
Last-Update: 2026-01-19

--- a/driver/XScreenSaver.ad.in
+++ b/driver/XScreenSaver.ad.in
@@ -160,6 +160,9 @@ XScreenSaver.bourneShell:           /bin/sh
 ! Whether typed passwords should echo as asterisks, or as nothing.
 *passwd.asterisks:     True
 
+! Force keyboard layout group for password entry: -1=disabled, 0-3=group number
+*passwdKeyboardGroup:  -1
+
 ! The default theme is similar to the Gtk defaults.
 !
 *default.Dialog.foreground:                    #000000
--- a/driver/auth.h
+++ b/driver/auth.h
@@ -86,6 +86,7 @@ extern Bool xscreensaver_auth_conv (void
                                     auth_response **resp);
 extern void xscreensaver_auth_finished (void *closure, Bool authenticated_p);
 extern void xscreensaver_splash (void *root_widget, Bool disable_settings_p);
+extern void xscreensaver_set_passwd_keyboard_group (int group);
 
 #endif /* __XSCREENSAVER_AUTH_H__ */
 
--- a/driver/demo-Gtk.c
+++ b/driver/demo-Gtk.c
@@ -223,6 +223,7 @@ G_DEFINE_TYPE (XScreenSaverApp, xscreens
   W(dpms_off_label)            \
   W(dpms_off_mlabel)           \
   W(fade_label)                        \
+  W(kbd_layout_combo)  \
   W(demo)                      \
   W(settings)                  \
 
@@ -1412,6 +1413,14 @@ flush_dialog_changes_and_save (state *s)
       }
   }
 
+  /* Keyboard layout combo. */
+  {
+    GtkComboBox *opt = GTK_COMBO_BOX (win->kbd_layout_combo);
+    int menu_elt = gtk_combo_box_get_active (opt);
+    if (menu_elt >= 0)
+      p2->passwd_keyboard_group = menu_elt - 1;  /* 0 -> -1, 1 -> 0, etc. */
+  }
+
   /* It is difficult to get "editing completed" events out of GtkEntry.
      I want something that fires on RET or focus-out, but I can't seem
      to find a consistent way to get that.  So let's fake it here.
@@ -1471,6 +1480,8 @@ flush_dialog_changes_and_save (state *s)
   COPY(grab_video_p);
   COPY(random_image_p);
 
+  COPY(passwd_keyboard_group);
+
   COPY(dialog_theme);
 # undef COPY
 
@@ -2702,6 +2713,24 @@ populate_prefs_page (state *s)
       }
   }
 
+  /* Keyboard layout combo */
+  {
+    GtkComboBox *cbox = GTK_COMBO_BOX (win->kbd_layout_combo);
+    int index;
+    if (p->passwd_keyboard_group >= PASSWD_KEYBOARD_GROUP_MIN &&
+        p->passwd_keyboard_group <= PASSWD_KEYBOARD_GROUP_MAX)
+      index = p->passwd_keyboard_group + 1;  /* 0->1, 1->2, 2->3, 3->4 */
+    else
+      index = 0;  /* Disabled */
+
+    /* Block the signal to prevent recursive calls */
+    gulong id = g_signal_handler_find (cbox, G_SIGNAL_MATCH_FUNC, 0, 0, 0,
+                                       G_CALLBACK (pref_changed_cb), 0);
+    if (id) g_signal_handler_block (cbox, id);
+    gtk_combo_box_set_active (cbox, index);
+    if (id) g_signal_handler_unblock (cbox, id);
+  }
+
   /* Map the `saver_mode' enum to mode menu to values. */
   {
     GtkComboBox *opt = GTK_COMBO_BOX (win->mode_menu);
--- a/driver/demo.ui
+++ b/driver/demo.ui
@@ -1835,6 +1835,66 @@ Under Wayland.</property>
                       </packing>
                     </child>
                     <child>
+                      <object class="GtkFrame" id="passwd_frame">
+                        <property name="visible">True</property>
+                        <property name="border-width">10</property>
+                        <property name="label-xalign">0</property>
+                        <property 
name="shadow-type">GTK_SHADOW_ETCHED_IN</property>
+                        <child type="label">
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="label" translatable="yes">Password 
Settings</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkGrid">
+                            <property name="visible">True</property>
+                            <property name="row-spacing">4</property>
+                            <property name="column-spacing">4</property>
+                            <property name="margin-start">12</property>
+                            <property name="margin-end">12</property>
+                            <property name="margin-top">12</property>
+                            <property name="margin-bottom">12</property>
+                            <child>
+                              <object class="GtkLabel">
+                                <property name="visible">True</property>
+                                <property name="label" 
translatable="yes">_Keyboard Layout for Password:</property>
+                                <property name="use-underline">True</property>
+                                <property name="xalign">1</property>
+                                <property 
name="mnemonic-widget">kbd_layout_combo</property>
+                              </object>
+                              <packing>
+                                <property name="left-attach">0</property>
+                                <property name="top-attach">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkComboBoxText" 
id="kbd_layout_combo">
+                                <property name="visible">True</property>
+                                <property name="can-focus">True</property>
+                                <signal handler="pref_changed_cb" 
name="changed"/>
+                                <items>
+                                  <item translatable="yes">Disabled (current 
layout)</item>
+                                  <item translatable="yes">Group 0 (first 
layout)</item>
+                                  <item translatable="yes">Group 1 (second 
layout)</item>
+                                  <item translatable="yes">Group 2 (third 
layout)</item>
+                                  <item translatable="yes">Group 3 (fourth 
layout)</item>
+                                </items>
+                              </object>
+                              <packing>
+                                <property name="left-attach">1</property>
+                                <property name="top-attach">0</property>
+                              </packing>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="left-attach">0</property>
+                        <property name="top-attach">2</property>
+                      </packing>
+                    </child>
+                    <child>
                       <object class="GtkFrame" id="dpms_frame">
                         <property name="name">dpms_frame</property>
                         <property name="border-width">10</property>
--- a/driver/dialog.c
+++ b/driver/dialog.c
@@ -190,6 +190,7 @@ struct window_state {
 
   double start_time, end_time;
   int passwd_timeout;
+  int passwd_keyboard_group;   /* Force keyboard group: -1=off, 0-3=group */
 
   Bool show_stars_p; /* "I regret that I have but one asterisk for my country."
                         -- Nathan Hale, 1776. */
@@ -1036,6 +1037,11 @@ window_init (Widget root_widget, int spl
   ws->passwd_timeout = get_seconds_resource (ws->dpy, "passwdTimeout", "Time");
   if (ws->passwd_timeout <= 5) ws->passwd_timeout = 5;
 
+  ws->passwd_keyboard_group = passwd_keyboard_group;
+  if (ws->passwd_keyboard_group < PASSWD_KEYBOARD_GROUP_MIN ||
+      ws->passwd_keyboard_group > PASSWD_KEYBOARD_GROUP_MAX)
+    ws->passwd_keyboard_group = PASSWD_KEYBOARD_GROUP_DISABLED;
+
   /* Put the version number in the label. */
   {
     char *version = strdup (screensaver_id + 17);
@@ -2100,8 +2106,54 @@ handle_keypress (window_state *ws, XKeyE
      XLookupString might not be UTF-8, and thus might not be compatible
      with XftDrawStringUtf8.
    */
-  int decoded_size = XLookupString (event, (char *)decoded, sizeof(decoded),
+
+  int decoded_size;
+
+# ifdef HAVE_XKB
+  if (ws->passwd_keyboard_group >= 0)
+    {
+      XkbDescPtr xkb = XkbGetMap(ws->dpy, XkbAllComponentsMask, XkbUseCoreKbd);
+      if (xkb)
+        {
+          unsigned int mods_rtrn = 0;
+          /* Force keyboard group by rebuilding the state with our group.
+             Use XkbBuildCoreState() from X11/extensions/XKB.h to combine
+             the current modifiers with the forced group. */
+          unsigned int mods = event->state & 0xff;
+          unsigned int forced_state = XkbBuildCoreState(mods, 
ws->passwd_keyboard_group);
+
+          /* Translate keycode using XKB with forced group state */
+          if (XkbTranslateKeyCode(xkb, event->keycode, forced_state,
+                                  &mods_rtrn, &keysym))
+            {
+              if (keysym != NoSymbol)
+                {
+                  /* Convert keysym to string */
+                  decoded_size = XkbTranslateKeySym(ws->dpy, &keysym, 0,
+                                                   (char *)decoded, 
sizeof(decoded),
+                                                   NULL);
+                  if (decoded_size < 0) decoded_size = 0;
+                }
+              else
+                decoded_size = 0;
+            }
+          else
+            decoded_size = 0;
+
+          XkbFreeKeyboard(xkb, XkbAllComponentsMask, True);
+        }
+      else
+        {
+          decoded_size = XLookupString (event, (char *)decoded, 
sizeof(decoded),
+                                       &keysym, &ws->compose_status);
+        }
+    }
+  else
+# endif
+    {
+      decoded_size = XLookupString (event, (char *)decoded, sizeof(decoded),
                                     &keysym, &ws->compose_status);
+    }
 
   if (decoded_size > MAX_BYTES_PER_CHAR)
     {
@@ -2552,6 +2604,15 @@ dialog_session (window_state *ws,
 /* To retain this across multiple calls from PAM to xscreensaver_auth_conv. */
 window_state *global_ws = 0;
 
+/* Keyboard group setting, set by xscreensaver_set_passwd_keyboard_group(). */
+static int passwd_keyboard_group = PASSWD_KEYBOARD_GROUP_DISABLED;
+
+void
+xscreensaver_set_passwd_keyboard_group (int group)
+{
+  passwd_keyboard_group = group;
+}
+
 
 /* The authentication conversation function.
 
--- a/driver/prefsw.c
+++ b/driver/prefsw.c
@@ -144,6 +144,7 @@ static const char * const prefs[] = {
   "lockVTs",                   /* not saved */
   "lockTimeout",
   "passwdTimeout",
+  "passwdKeyboardGroup",
   "visualID",
   "installColormap",
   "verbose",
@@ -582,6 +583,7 @@ write_init_file (Display *dpy,
       CHECK("lockVTs")         continue;  /* don't save, unused */
       CHECK("lockTimeout")     type = pref_time, t = p->lock_timeout;
       CHECK("passwdTimeout")   type = pref_time, t = p->passwd_timeout;
+      CHECK("passwdKeyboardGroup") type = pref_int, i = 
p->passwd_keyboard_group;
       CHECK("visualID")                type = pref_str,  s =    visual_name;
       CHECK("installColormap") type = pref_bool, b = p->install_cmap_p;
       CHECK("verbose")         type = pref_bool, b = p->verbose_p;
@@ -845,6 +847,11 @@ load_init_file (Display *dpy, saver_pref
   p->lock_timeout    = 1000 * get_minutes_resource (dpy, "lockTimeout", 
"Time");
   p->cycle           = 1000 * get_minutes_resource (dpy, "cycle", "Time");
   p->passwd_timeout  = 1000 * get_seconds_resource (dpy, "passwdTimeout", 
"Time");
+  p->passwd_keyboard_group = get_integer_resource (dpy, "passwdKeyboardGroup", 
"Integer");
+  if (p->passwd_keyboard_group < PASSWD_KEYBOARD_GROUP_MIN-1 ||
+      p->passwd_keyboard_group > PASSWD_KEYBOARD_GROUP_MAX)
+    p->passwd_keyboard_group = PASSWD_KEYBOARD_GROUP_DISABLED;
+  "passwdKeyboardGroup",
   p->pointer_hysteresis = get_integer_resource (dpy, 
"pointerHysteresis","Integer");
 
   p->dpms_enabled_p  = get_boolean_resource (dpy, "dpmsEnabled", "Boolean");
--- a/driver/types.h
+++ b/driver/types.h
@@ -30,6 +30,14 @@ typedef enum {
   TEXT_DATE, TEXT_LITERAL, TEXT_FILE, TEXT_PROGRAM, TEXT_URL
 } text_mode;
 
+/* passwd_keyboard_group values:
+   -1 = disabled (use current keyboard layout)
+    0-3 = force XKB group 0-3
+   See X11/extensions/XKB.h for XkbGroup1Index..XkbGroup4Index constants. */
+#define PASSWD_KEYBOARD_GROUP_DISABLED  -1
+#define PASSWD_KEYBOARD_GROUP_MIN        0
+#define PASSWD_KEYBOARD_GROUP_MAX        3
+
 typedef struct saver_preferences saver_preferences;
 typedef struct saver_screen_info saver_screen_info;
 
@@ -78,6 +86,7 @@ struct saver_preferences {
   Time cycle;                  /* how long each hack should run */
   Time passwd_timeout;         /* how long before pw dialog goes down */
   Time watchdog_timeout;       /* how often to re-raise and re-blank screen */
+  int passwd_keyboard_group;   /* force keyboard group for password: -1=off, 
0-3=group */
   int pointer_hysteresis;      /* mouse motions less than N/sec are ignored */
 
   Bool dpms_enabled_p;         /* whether to power down the monitor */
--- a/driver/xscreensaver-auth.c
+++ b/driver/xscreensaver-auth.c
@@ -70,6 +70,7 @@
 #include "resources.h"
 #include "visual.h"
 #include "auth.h"
+#include "prefs.h"
 
 #ifdef __GNUC__
  __extension__     /* shut up about "string length is greater than the length
@@ -86,6 +87,21 @@ static char *defaults[] = {
 char *progclass = 0;
 Bool debug_p = False;
 
+static int passwd_keyboard_group = PASSWD_KEYBOARD_GROUP_DISABLED;
+
+
+/* Handler for parse_init_file() to read only what we need from 
~/.xscreensaver */
+static void
+auth_line_handler (int lineno, const char *key, const char *val, void *closure)
+{
+  if (!strcmp (key, "passwdKeyboardGroup"))
+    {
+      int v = atoi (val);
+      if (v >= PASSWD_KEYBOARD_GROUP_MIN-1 && v <= PASSWD_KEYBOARD_GROUP_MAX)
+        passwd_keyboard_group = v;
+    }
+}
+
 
 #ifdef HAVE_PROC_OOM
 /* Linux systems have an "out-of-memory killer" that will nuke random procs in
@@ -380,6 +396,21 @@ main (int argc, char **argv)
   if (xsync_p) XSynchronize (dpy, True);
   init_xscreensaver_atoms (dpy);
 
+  /* Read configuration from ~/.xscreensaver */
+  {
+    const char *home = getenv("HOME");
+    if (home)
+      {
+        char *fn = (char *) malloc (strlen(home) + 40);
+        sprintf (fn, "%s/.xscreensaver", home);
+        parse_init_file (fn, auth_line_handler, 0);
+        free (fn);
+      }
+  }
+
+  /* Pass the keyboard group setting to dialog.c */
+  xscreensaver_set_passwd_keyboard_group (passwd_keyboard_group);
+
   if (splash_p == 1 || init_p)
     dpms_init (dpy);
 
--- a/driver/xscreensaver.man
+++ b/driver/xscreensaver.man
@@ -527,6 +527,11 @@ If the screen is locked, then this is ho
 should be left on the screen before giving up (default 30 seconds).  A few
 seconds are added each time you type a character.
 .TP 8
+.B passwdKeyboardGroup\fP (class \fBPasswd.Integer\fP)
+Force specific keyboard layout group for password entry.
+Values: -1 (disabled, use current layout), 0-3 (XKB group number).
+Default: -1.
+.TP 8
 .B dpmsEnabled\fP (class \fBBoolean\fP)
 Whether power management is enabled.
 .TP 8
---END PATCH---

Reply via email to