@@ -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