@@ -25,6 +25,8 @@ import curses.textpad
2525import sys
2626import signal
2727import os
28+ import shutil
29+ import tempfile
2830import logging
2931import subprocess
3032from typing import List , Any , Tuple , Generator
@@ -102,11 +104,63 @@ def get_keyboard_layout() -> str:
102104
103105def set_keyboard_layout (layout : str ) -> subprocess .CompletedProcess :
104106 """
105- Set the keyboard layout based on the user selection.
107+ Set the keyboard layout by editing /etc/default/keyboard and applying it with setupcon.
108+ This avoids localectl (which is disabled on Debian/Ubuntu builds).
109+
110+ Notes:
111+ - Does NOT trigger update-initramfs; only applies to the running system's console.
106112 """
107- cmd : List [str ] = ['localectl' , 'set-x11-keymap' , layout , 'pc105' ]
108- subprocess .run (cmd , shell = False , check = True )
109- return subprocess .run ('setupcon' , shell = False , check = True )
113+ kb_path = "/etc/default/keyboard"
114+ kb_backup = kb_path + ".bak"
115+
116+ # Read current content (if file doesn't exist, start with a minimal stub)
117+ try :
118+ with open (kb_path , "r" , encoding = "utf-8" ) as f :
119+ lines = f .readlines ()
120+ except FileNotFoundError :
121+ lines = [
122+ 'XKBMODEL="pc105"\n ' ,
123+ f'XKBLAYOUT="{ layout } "\n ' ,
124+ 'XKBVARIANT=""\n ' ,
125+ 'XKBOPTIONS=""\n ' ,
126+ 'BACKSPACE="guess"\n ' ,
127+ ]
128+ else :
129+ # Update or append XKBLAYOUT line
130+ updated = False
131+ pattern = re .compile (r'^\s*XKBLAYOUT\s*=' )
132+ for i , line in enumerate (lines ):
133+ if pattern .match (line ):
134+ lines [i ] = f'XKBLAYOUT="{ layout } "\n '
135+ updated = True
136+ break
137+ if not updated :
138+ # Append if not present
139+ lines .append (f'XKBLAYOUT="{ layout } "\n ' )
140+
141+ # Atomic write with backup
142+ os .makedirs (os .path .dirname (kb_path ), exist_ok = True )
143+ if os .path .exists (kb_path ):
144+ shutil .copy2 (kb_path , kb_backup )
145+
146+ fd , tmp = tempfile .mkstemp (prefix = ".keyboard." , dir = os .path .dirname (kb_path ))
147+ try :
148+ with os .fdopen (fd , "w" , encoding = "utf-8" ) as f :
149+ f .writelines (lines )
150+ os .replace (tmp , kb_path )
151+ except Exception :
152+ # Clean temp file and restore from backup if we wrote a partial file
153+ try :
154+ os .remove (tmp )
155+ except OSError :
156+ pass
157+ if os .path .exists (kb_backup ):
158+ shutil .copy2 (kb_backup , kb_path )
159+ raise
160+
161+ # Apply to current console immediately (does not affect serial consoles)
162+ # Use --force so it rebuilds/loads even if it thinks nothing changed.
163+ return subprocess .run (["setupcon" , "--force" ], shell = False , check = True )
110164
111165
112166def get_valid_keyboard_layouts () -> List [str ]:
0 commit comments