1 /* === This file is part of Calamares - <https://calamares.io> ===
2 *
3 * SPDX-FileCopyrightText: 2011 Lennart Poettering
4 * SPDX-FileCopyrightText: Kay Sievers
5 * SPDX-FileCopyrightText: 2014-2016 Teo Mrnjavac <teo@kde.org>
6 * SPDX-FileCopyrightText: 2014 Kevin Kofler <kevin.kofler@chello.at>
7 * SPDX-License-Identifier: GPL-3.0-or-later
8 *
9 * Portions from systemd (localed.c):
10 * Copyright 2011 Lennart Poettering
11 * Copyright 2013 Kay Sievers
12 * (originally under LGPLv2.1+, used under the LGPL to GPL conversion clause)
13 *
14 * Calamares is Free Software: see the License-Identifier above.
15 *
16 */
17
18 #include "SetKeyboardLayoutJob.h"
19
20 #include "GlobalStorage.h"
21 #include "JobQueue.h"
22 #include "utils/CalamaresUtilsSystem.h"
23 #include "utils/Logger.h"
24 #include "utils/String.h"
25
26 #include <QDir>
27 #include <QFile>
28 #include <QFileInfo>
29 #include <QSettings>
30 #include <QTextStream>
31
32
SetKeyboardLayoutJob(const QString & model,const QString & layout,const QString & variant,const AdditionalLayoutInfo & additionalLayoutInfo,const QString & xOrgConfFileName,const QString & convertedKeymapPath,bool writeEtcDefaultKeyboard)33 SetKeyboardLayoutJob::SetKeyboardLayoutJob( const QString& model,
34 const QString& layout,
35 const QString& variant,
36 const AdditionalLayoutInfo& additionalLayoutInfo,
37 const QString& xOrgConfFileName,
38 const QString& convertedKeymapPath,
39 bool writeEtcDefaultKeyboard )
40 : Calamares::Job()
41 , m_model( model )
42 , m_layout( layout )
43 , m_variant( variant )
44 , m_additionalLayoutInfo( additionalLayoutInfo )
45 , m_xOrgConfFileName( xOrgConfFileName )
46 , m_convertedKeymapPath( convertedKeymapPath )
47 , m_writeEtcDefaultKeyboard( writeEtcDefaultKeyboard )
48 {
49 }
50
51
52 QString
prettyName() const53 SetKeyboardLayoutJob::prettyName() const
54 {
55 return tr( "Set keyboard model to %1, layout to %2-%3" ).arg( m_model ).arg( m_layout ).arg( m_variant );
56 }
57
58
59 QString
findConvertedKeymap(const QString & convertedKeymapPath) const60 SetKeyboardLayoutJob::findConvertedKeymap( const QString& convertedKeymapPath ) const
61 {
62 cDebug() << "Looking for converted keymap in" << convertedKeymapPath;
63
64 // No search path supplied, assume the distribution does not provide
65 // converted keymaps
66 if ( convertedKeymapPath.isEmpty() )
67 {
68 return QString();
69 }
70
71 QDir convertedKeymapDir( convertedKeymapPath );
72 QString name = m_variant.isEmpty() ? m_layout : ( m_layout + '-' + m_variant );
73
74 if ( convertedKeymapDir.exists( name + ".map" ) || convertedKeymapDir.exists( name + ".map.gz" ) )
75 {
76 cDebug() << Logger::SubEntry << "Found converted keymap" << name;
77 return name;
78 }
79
80 return QString();
81 }
82
83
84 STATICTEST QString
findLegacyKeymap(const QString & layout,const QString & model,const QString & variant)85 findLegacyKeymap( const QString& layout, const QString& model, const QString& variant )
86 {
87 cDebug() << "Looking for legacy keymap" << layout << model << variant << "in QRC";
88
89 int bestMatching = 0;
90 QString name;
91
92 QFile file( ":/kbd-model-map" );
93 if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) )
94 {
95 cDebug() << Logger::SubEntry << "Could not read QRC";
96 return QString();
97 }
98
99 QTextStream stream( &file );
100 while ( !stream.atEnd() )
101 {
102 QString line = stream.readLine().trimmed();
103 if ( line.isEmpty() || line.startsWith( '#' ) )
104 {
105 continue;
106 }
107
108 QStringList mapping = line.split( '\t', SplitSkipEmptyParts );
109 if ( mapping.size() < 5 )
110 {
111 continue;
112 }
113
114 int matching = 0;
115
116 // Determine how well matching this entry is
117 // We assume here that we have one X11 layout. If the UI changes to
118 // allow more than one layout, this should change too.
119 if ( layout == mapping[ 1 ] )
120 // If we got an exact match, this is best
121 {
122 matching = 10;
123 }
124 // Look for an entry whose first layout matches ours
125 else if ( mapping[ 1 ].startsWith( layout + ',' ) )
126 {
127 matching = 5;
128 }
129
130 if ( matching > 0 )
131 {
132 if ( model.isEmpty() || model == mapping[ 2 ] )
133 {
134 matching++;
135 }
136
137 QString mappingVariant = mapping[ 3 ];
138 if ( mappingVariant == "-" )
139 {
140 mappingVariant = QString();
141 }
142 else if ( mappingVariant.startsWith( ',' ) )
143 {
144 mappingVariant.remove( 1, 0 );
145 }
146
147 if ( variant == mappingVariant )
148 {
149 matching++;
150 }
151
152 // We ignore mapping[4], the xkb options, for now. If we ever
153 // allow setting options in the UI, we should match them here.
154 }
155
156 // The best matching entry so far, then let's save that
157 if ( matching >= qMax( bestMatching, 1 ) )
158 {
159 cDebug() << Logger::SubEntry << "Found legacy keymap" << mapping[ 0 ] << "with score" << matching;
160
161 if ( matching > bestMatching )
162 {
163 bestMatching = matching;
164 name = mapping[ 0 ];
165 }
166 }
167 }
168
169 return name;
170 }
171
172 QString
findLegacyKeymap() const173 SetKeyboardLayoutJob::findLegacyKeymap() const
174 {
175 return ::findLegacyKeymap( m_layout, m_model, m_variant );
176 }
177
178
179 bool
writeVConsoleData(const QString & vconsoleConfPath,const QString & convertedKeymapPath) const180 SetKeyboardLayoutJob::writeVConsoleData( const QString& vconsoleConfPath, const QString& convertedKeymapPath ) const
181 {
182 cDebug() << "Writing vconsole data to" << vconsoleConfPath;
183
184 QString keymap = findConvertedKeymap( convertedKeymapPath );
185 if ( keymap.isEmpty() )
186 {
187 keymap = findLegacyKeymap();
188 }
189 if ( keymap.isEmpty() )
190 {
191 cDebug() << "Trying to use X11 layout" << m_layout << "as the virtual console layout";
192 keymap = m_layout;
193 }
194
195 QStringList existingLines;
196
197 // Read in the existing vconsole.conf, if it exists
198 QFile file( vconsoleConfPath );
199 if ( file.exists() )
200 {
201 file.open( QIODevice::ReadOnly | QIODevice::Text );
202 QTextStream stream( &file );
203 while ( !stream.atEnd() )
204 {
205 existingLines << stream.readLine();
206 }
207 file.close();
208 if ( stream.status() != QTextStream::Ok )
209 {
210 cError() << "Could not read lines from" << file.fileName();
211 return false;
212 }
213 }
214
215 // Write out the existing lines and replace the KEYMAP= line
216 if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
217 {
218 cError() << "Could not open" << file.fileName() << "for writing.";
219 return false;
220 }
221 QTextStream stream( &file );
222 bool found = false;
223 for ( const QString& existingLine : qAsConst( existingLines ) )
224 {
225 if ( existingLine.trimmed().startsWith( "KEYMAP=" ) )
226 {
227 stream << "KEYMAP=" << keymap << '\n';
228 found = true;
229 }
230 else
231 {
232 stream << existingLine << '\n';
233 }
234 }
235 // Add a KEYMAP= line if there wasn't any
236 if ( !found )
237 {
238 stream << "KEYMAP=" << keymap << '\n';
239 }
240 stream.flush();
241 file.close();
242
243 cDebug() << Logger::SubEntry << "Written KEYMAP=" << keymap << "to vconsole.conf" << stream.status();
244
245 return ( stream.status() == QTextStream::Ok );
246 }
247
248
249 bool
writeX11Data(const QString & keyboardConfPath) const250 SetKeyboardLayoutJob::writeX11Data( const QString& keyboardConfPath ) const
251 {
252 cDebug() << "Writing X11 configuration to" << keyboardConfPath;
253
254 QFile file( keyboardConfPath );
255 if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
256 {
257 cError() << "Could not open" << file.fileName() << "for writing.";
258 return false;
259 }
260 QTextStream stream( &file );
261
262 stream << "# Read and parsed by systemd-localed. It's probably wise not to edit this file\n"
263 "# manually too freely.\n"
264 "Section \"InputClass\"\n"
265 " Identifier \"system-keyboard\"\n"
266 " MatchIsKeyboard \"on\"\n";
267
268
269 if ( m_additionalLayoutInfo.additionalLayout.isEmpty() )
270 {
271 if ( !m_layout.isEmpty() )
272 {
273 stream << " Option \"XkbLayout\" \"" << m_layout << "\"\n";
274 }
275
276 if ( !m_variant.isEmpty() )
277 {
278 stream << " Option \"XkbVariant\" \"" << m_variant << "\"\n";
279 }
280 }
281 else
282 {
283 if ( !m_layout.isEmpty() )
284 {
285 stream << " Option \"XkbLayout\" \"" << m_additionalLayoutInfo.additionalLayout << "," << m_layout
286 << "\"\n";
287 }
288
289 if ( !m_variant.isEmpty() )
290 {
291 stream << " Option \"XkbVariant\" \"" << m_additionalLayoutInfo.additionalVariant << "," << m_variant
292 << "\"\n";
293 }
294
295 stream << " Option \"XkbOptions\" \"" << m_additionalLayoutInfo.groupSwitcher << "\"\n";
296 }
297
298 stream << "EndSection\n";
299 stream.flush();
300
301 file.close();
302
303 cDebug() << Logger::SubEntry << "Written XkbLayout" << m_layout << "; XkbModel" << m_model << "; XkbVariant"
304 << m_variant << "to X.org file" << keyboardConfPath << stream.status();
305
306 return ( stream.status() == QTextStream::Ok );
307 }
308
309
310 bool
writeDefaultKeyboardData(const QString & defaultKeyboardPath) const311 SetKeyboardLayoutJob::writeDefaultKeyboardData( const QString& defaultKeyboardPath ) const
312 {
313 cDebug() << "Writing default keyboard data to" << defaultKeyboardPath;
314
315 QFile file( defaultKeyboardPath );
316 if ( !file.open( QIODevice::WriteOnly | QIODevice::Text ) )
317 {
318 cError() << "Could not open" << defaultKeyboardPath << "for writing";
319 return false;
320 }
321 QTextStream stream( &file );
322
323 stream << "# KEYBOARD CONFIGURATION FILE\n\n"
324 "# Consult the keyboard(5) manual page.\n\n";
325
326 stream << "XKBMODEL=\"" << m_model << "\"\n";
327 stream << "XKBLAYOUT=\"" << m_layout << "\"\n";
328 stream << "XKBVARIANT=\"" << m_variant << "\"\n";
329 stream << "XKBOPTIONS=\"\"\n\n";
330 stream << "BACKSPACE=\"guess\"\n";
331 stream.flush();
332
333 file.close();
334
335 cDebug() << Logger::SubEntry << "Written XKBMODEL" << m_model << "; XKBLAYOUT" << m_layout << "; XKBVARIANT"
336 << m_variant << "to /etc/default/keyboard file" << defaultKeyboardPath << stream.status();
337
338 return ( stream.status() == QTextStream::Ok );
339 }
340
341
342 Calamares::JobResult
exec()343 SetKeyboardLayoutJob::exec()
344 {
345 cDebug() << "Executing SetKeyboardLayoutJob";
346 // Read the location of the destination's / in the host file system from
347 // the global settings
348 Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
349 QDir destDir( gs->value( "rootMountPoint" ).toString() );
350
351 {
352 // Get the path to the destination's /etc/vconsole.conf
353 QString vconsoleConfPath = destDir.absoluteFilePath( "etc/vconsole.conf" );
354
355 // Get the path to the destination's path to the converted key mappings
356 QString convertedKeymapPath = m_convertedKeymapPath;
357 if ( !convertedKeymapPath.isEmpty() )
358 {
359 while ( convertedKeymapPath.startsWith( '/' ) )
360 {
361 convertedKeymapPath.remove( 0, 1 );
362 }
363 convertedKeymapPath = destDir.absoluteFilePath( convertedKeymapPath );
364 }
365
366 if ( !writeVConsoleData( vconsoleConfPath, convertedKeymapPath ) )
367 {
368 return Calamares::JobResult::error( tr( "Failed to write keyboard configuration for the virtual console." ),
369 tr( "Failed to write to %1" ).arg( vconsoleConfPath ) );
370 }
371 }
372
373 {
374 // Get the path to the destination's /etc/X11/xorg.conf.d/00-keyboard.conf
375 QString xorgConfDPath;
376 QString keyboardConfPath;
377 if ( QDir::isAbsolutePath( m_xOrgConfFileName ) )
378 {
379 keyboardConfPath = m_xOrgConfFileName;
380 while ( keyboardConfPath.startsWith( '/' ) )
381 {
382 keyboardConfPath.remove( 0, 1 );
383 }
384 keyboardConfPath = destDir.absoluteFilePath( keyboardConfPath );
385 xorgConfDPath = QFileInfo( keyboardConfPath ).path();
386 }
387 else
388 {
389 xorgConfDPath = destDir.absoluteFilePath( "etc/X11/xorg.conf.d" );
390 keyboardConfPath = QDir( xorgConfDPath ).absoluteFilePath( m_xOrgConfFileName );
391 }
392 destDir.mkpath( xorgConfDPath );
393
394 if ( !writeX11Data( keyboardConfPath ) )
395 {
396 return Calamares::JobResult::error( tr( "Failed to write keyboard configuration for X11." ),
397 tr( "Failed to write to %1" ).arg( keyboardConfPath ) );
398 }
399 }
400
401 {
402 QString defaultKeyboardPath;
403 if ( QDir( destDir.absoluteFilePath( "etc/default" ) ).exists() )
404 {
405 defaultKeyboardPath = destDir.absoluteFilePath( "etc/default/keyboard" );
406 }
407
408 if ( !defaultKeyboardPath.isEmpty() && m_writeEtcDefaultKeyboard )
409 {
410 if ( !writeDefaultKeyboardData( defaultKeyboardPath ) )
411 {
412 return Calamares::JobResult::error(
413 tr( "Failed to write keyboard configuration to existing /etc/default directory." ),
414 tr( "Failed to write to %1" ).arg( defaultKeyboardPath ) );
415 }
416 }
417 }
418
419 return Calamares::JobResult::ok();
420 }
421