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