1 /*
2 This source file is part of Konsole, a terminal emulator.
3
4 SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
5
6 SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9 // Own
10 #include "ProfileManager.h"
11 #include "PopStackOnExit.h"
12
13 #include "konsoledebug.h"
14
15 // Qt
16 #include <QDir>
17 #include <QFileInfo>
18 #include <QString>
19
20 // KDE
21 #include <KConfig>
22 #include <KConfigGroup>
23 #include <KLocalizedString>
24 #include <KMessageBox>
25
26 // Konsole
27 #include "ProfileGroup.h"
28 #include "ProfileModel.h"
29 #include "ProfileReader.h"
30 #include "ProfileWriter.h"
31
32 using namespace Konsole;
33
stringLessThan(const QString & p1,const QString & p2)34 static bool stringLessThan(const QString &p1, const QString &p2)
35 {
36 return QString::localeAwareCompare(p1, p2) < 0;
37 }
38
profileNameLessThan(const Profile::Ptr & p1,const Profile::Ptr & p2)39 static bool profileNameLessThan(const Profile::Ptr &p1, const Profile::Ptr &p2)
40 {
41 // Always put the Default/fallback profile at the top
42 if (p1->isFallback()) {
43 return true;
44 } else if (p2->isFallback()) {
45 return false;
46 }
47
48 return stringLessThan(p1->name(), p2->name());
49 }
50
ProfileManager()51 ProfileManager::ProfileManager()
52 : m_config(KSharedConfig::openConfig())
53 {
54 // load fallback profile
55 initFallbackProfile();
56 _defaultProfile = _fallbackProfile;
57
58 // lookup the default profile specified in <App>rc
59 // For stand-alone Konsole, m_config is just "konsolerc"
60 // For konsolepart, m_config might be "yakuakerc", "dolphinrc", "katerc"...
61 KConfigGroup group = m_config->group("Desktop Entry");
62 QString defaultProfileFileName = group.readEntry("DefaultProfile", "");
63
64 // if the hosting application of konsolepart does not specify its own
65 // default profile, use the default profile of stand-alone Konsole.
66 if (defaultProfileFileName.isEmpty()) {
67 KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig(QStringLiteral("konsolerc"));
68 group = konsoleConfig->group("Desktop Entry");
69 defaultProfileFileName = group.readEntry("DefaultProfile", "");
70 }
71
72 loadAllProfiles(defaultProfileFileName);
73 loadShortcuts();
74
75 Q_ASSERT(_profiles.size() > 0);
76 Q_ASSERT(_defaultProfile);
77 }
78
79 ProfileManager::~ProfileManager() = default;
80
Q_GLOBAL_STATIC(ProfileManager,theProfileManager)81 Q_GLOBAL_STATIC(ProfileManager, theProfileManager)
82 ProfileManager *ProfileManager::instance()
83 {
84 return theProfileManager;
85 }
86
findProfile(const Profile::Ptr & profile) const87 ProfileManager::Iterator ProfileManager::findProfile(const Profile::Ptr &profile) const
88 {
89 return std::find(_profiles.cbegin(), _profiles.cend(), profile);
90 }
91
initFallbackProfile()92 void ProfileManager::initFallbackProfile()
93 {
94 _fallbackProfile = Profile::Ptr(new Profile());
95 _fallbackProfile->useFallback();
96 addProfile(_fallbackProfile);
97 }
98
loadProfile(const QString & shortPath)99 Profile::Ptr ProfileManager::loadProfile(const QString &shortPath)
100 {
101 // the fallback profile has a 'special' path name, "FALLBACK/"
102 if (shortPath == _fallbackProfile->path()) {
103 return _fallbackProfile;
104 }
105
106 QString path = shortPath;
107
108 // add a suggested suffix and relative prefix if missing
109 QFileInfo fileInfo(path);
110
111 if (fileInfo.isDir()) {
112 return Profile::Ptr();
113 }
114
115 if (fileInfo.suffix() != QLatin1String("profile")) {
116 path.append(QLatin1String(".profile"));
117 }
118 if (fileInfo.path().isEmpty() || fileInfo.path() == QLatin1String(".")) {
119 path.prepend(QLatin1String("konsole") + QDir::separator());
120 }
121
122 // if the file is not an absolute path, look it up
123 if (!fileInfo.isAbsolute()) {
124 path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, path);
125 }
126
127 // if the file is not found, return immediately
128 if (path.isEmpty()) {
129 return Profile::Ptr();
130 }
131
132 // check that we have not already loaded this profile
133 for (const Profile::Ptr &profile : _profiles) {
134 if (profile->path() == path) {
135 return profile;
136 }
137 }
138
139 // guard to prevent problems if a profile specifies itself as its parent
140 // or if there is recursion in the "inheritance" chain
141 // (eg. two profiles, A and B, specifying each other as their parents)
142 static QStack<QString> recursionGuard;
143 PopStackOnExit<QString> popGuardOnExit(recursionGuard);
144
145 if (recursionGuard.contains(path)) {
146 qCDebug(KonsoleDebug) << "Ignoring attempt to load profile recursively from" << path;
147 return _fallbackProfile;
148 }
149 recursionGuard.push(path);
150
151 // load the profile
152 ProfileReader reader;
153
154 Profile::Ptr newProfile = Profile::Ptr(new Profile(fallbackProfile()));
155 newProfile->setProperty(Profile::Path, path);
156
157 QString parentProfilePath;
158 bool result = reader.readProfile(path, newProfile, parentProfilePath);
159
160 if (!parentProfilePath.isEmpty()) {
161 Profile::Ptr parentProfile = loadProfile(parentProfilePath);
162 newProfile->setParent(parentProfile);
163 }
164
165 if (!result) {
166 qCDebug(KonsoleDebug) << "Could not load profile from " << path;
167 return Profile::Ptr();
168 } else if (newProfile->name().isEmpty()) {
169 qCWarning(KonsoleDebug) << path << " does not have a valid name, ignoring.";
170 return Profile::Ptr();
171 } else {
172 addProfile(newProfile);
173 return newProfile;
174 }
175 }
availableProfilePaths() const176 QStringList ProfileManager::availableProfilePaths() const
177 {
178 ProfileReader reader;
179
180 QStringList paths;
181 paths += reader.findProfiles();
182
183 std::stable_sort(paths.begin(), paths.end(), stringLessThan);
184
185 return paths;
186 }
187
availableProfileNames() const188 QStringList ProfileManager::availableProfileNames() const
189 {
190 QStringList names;
191
192 const QList<Profile::Ptr> allProfiles = ProfileManager::instance()->allProfiles();
193 for (const Profile::Ptr &profile : allProfiles) {
194 if (!profile->isHidden()) {
195 names.push_back(profile->name());
196 }
197 }
198
199 std::stable_sort(names.begin(), names.end(), stringLessThan);
200
201 return names;
202 }
203
loadAllProfiles(const QString & defaultProfileFileName)204 void ProfileManager::loadAllProfiles(const QString &defaultProfileFileName)
205 {
206 const QStringList &paths = availableProfilePaths();
207 for (const QString &path : paths) {
208 Profile::Ptr profile = loadProfile(path);
209 if (profile && !defaultProfileFileName.isEmpty() && path.endsWith(QLatin1Char('/') + defaultProfileFileName)) {
210 _defaultProfile = profile;
211 }
212 }
213 }
214
saveSettings()215 void ProfileManager::saveSettings()
216 {
217 saveShortcuts();
218 }
219
sortProfiles()220 void ProfileManager::sortProfiles()
221 {
222 std::sort(_profiles.begin(), _profiles.end(), profileNameLessThan);
223 }
224
allProfiles()225 QList<Profile::Ptr> ProfileManager::allProfiles()
226 {
227 sortProfiles();
228 return loadedProfiles();
229 }
230
loadedProfiles() const231 QList<Profile::Ptr> ProfileManager::loadedProfiles() const
232 {
233 return {_profiles.cbegin(), _profiles.cend()};
234 }
235
defaultProfile() const236 Profile::Ptr ProfileManager::defaultProfile() const
237 {
238 return _defaultProfile;
239 }
fallbackProfile() const240 Profile::Ptr ProfileManager::fallbackProfile() const
241 {
242 return _fallbackProfile;
243 }
244
generateUniqueName() const245 QString ProfileManager::generateUniqueName() const
246 {
247 const QStringList existingProfileNames = availableProfileNames();
248 int nameSuffix = 1;
249 QString uniqueProfileName;
250 do {
251 uniqueProfileName = QStringLiteral("Profile ") + QString::number(nameSuffix);
252 ++nameSuffix;
253 } while (existingProfileNames.contains(uniqueProfileName));
254
255 return uniqueProfileName;
256 }
257
saveProfile(const Profile::Ptr & profile)258 QString ProfileManager::saveProfile(const Profile::Ptr &profile)
259 {
260 ProfileWriter writer;
261
262 QString newPath = writer.getPath(profile);
263
264 if (!writer.writeProfile(newPath, profile)) {
265 KMessageBox::sorry(nullptr, i18n("Konsole does not have permission to save this profile to %1", newPath));
266 }
267
268 return newPath;
269 }
270
changeProfile(Profile::Ptr profile,QHash<Profile::Property,QVariant> propertyMap,bool persistent)271 void ProfileManager::changeProfile(Profile::Ptr profile, QHash<Profile::Property, QVariant> propertyMap, bool persistent)
272 {
273 Q_ASSERT(profile);
274
275 const QString origPath = profile->path();
276 const QKeySequence origShortcut = shortcut(profile);
277 const bool isDefaultProfile = profile == defaultProfile();
278
279 const QString uniqueProfileName = generateUniqueName();
280
281 // Don't save a profile with an empty name on disk
282 persistent = persistent && !profile->name().isEmpty();
283
284 bool messageShown = false;
285 bool isNameChanged = false;
286 // Insert the changes into the existing Profile instance
287 for (auto it = propertyMap.cbegin(); it != propertyMap.cend(); ++it) {
288 const auto property = it.key();
289 auto value = it.value();
290
291 isNameChanged = property == Profile::Name || property == Profile::UntranslatedName;
292
293 // "Default" is reserved for the fallback profile, override it;
294 // The message is only shown if the user manually typed "Default"
295 // in the name box in the edit profile dialog; i.e. saving the
296 // fallback profile where the user didn't change the name at all,
297 // the uniqueProfileName is used silently a couple of lines above.
298 if (isNameChanged && value == QLatin1String("Default")) {
299 value = uniqueProfileName;
300 if (!messageShown) {
301 KMessageBox::sorry(nullptr,
302 i18n("The name \"Default\" is reserved for the built-in"
303 " fallback profile;\nthe profile is going to be"
304 " saved as \"%1\"",
305 uniqueProfileName));
306 messageShown = true;
307 }
308 }
309
310 profile->setProperty(property, value);
311 }
312
313 // when changing a group, iterate through the profiles
314 // in the group and call changeProfile() on each of them
315 //
316 // this is so that each profile in the group, the profile is
317 // applied, a change notification is emitted and the profile
318 // is saved to disk
319 ProfileGroup::Ptr group = profile->asGroup();
320 if (group) {
321 const QList<Profile::Ptr> profiles = group->profiles();
322 for (const Profile::Ptr &groupProfile : profiles) {
323 changeProfile(groupProfile, propertyMap, persistent);
324 }
325 return;
326 }
327
328 // save changes to disk, unless the profile is hidden, in which case
329 // it has no file on disk
330 if (persistent && !profile->isHidden()) {
331 profile->setProperty(Profile::Path, saveProfile(profile));
332 }
333
334 if (isNameChanged) { // Renamed?
335 // origPath is empty when saving a new profile
336 if (!origPath.isEmpty()) {
337 // Delete the old/redundant .profile from disk
338 QFile::remove(origPath);
339
340 // Change the default profile name to the new one
341 if (isDefaultProfile) {
342 setDefaultProfile(profile);
343 }
344
345 // If the profile had a shortcut, re-assign it to the profile
346 if (!origShortcut.isEmpty()) {
347 setShortcut(profile, origShortcut);
348 }
349 }
350
351 sortProfiles();
352 }
353
354 // Notify the world about the change
355 Q_EMIT profileChanged(profile);
356 }
357
addProfile(const Profile::Ptr & profile)358 void ProfileManager::addProfile(const Profile::Ptr &profile)
359 {
360 if (_profiles.empty()) {
361 _defaultProfile = profile;
362 }
363
364 if (findProfile(profile) == _profiles.cend()) {
365 _profiles.push_back(profile);
366 Q_EMIT profileAdded(profile);
367 }
368 }
369
deleteProfile(Profile::Ptr profile)370 bool ProfileManager::deleteProfile(Profile::Ptr profile)
371 {
372 bool wasDefault = (profile == defaultProfile());
373
374 if (profile) {
375 // try to delete the config file
376 if (profile->isPropertySet(Profile::Path) && QFile::exists(profile->path())) {
377 if (!QFile::remove(profile->path())) {
378 qCDebug(KonsoleDebug) << "Could not delete profile: " << profile->path() << "The file is most likely in a directory which is read-only.";
379
380 return false;
381 }
382 }
383
384 setShortcut(profile, QKeySequence());
385 if (auto it = findProfile(profile); it != _profiles.end()) {
386 _profiles.erase(it);
387 }
388
389 // mark the profile as hidden so that it does not show up in the
390 // Manage Profiles dialog and is not saved to disk
391 profile->setHidden(true);
392 }
393
394 // If we just deleted the default profile, replace it with the first
395 // profile in the list.
396 if (wasDefault) {
397 const QList<Profile::Ptr> existingProfiles = allProfiles();
398 setDefaultProfile(existingProfiles.at(0));
399 }
400
401 Q_EMIT profileRemoved(profile);
402
403 return true;
404 }
405
setDefaultProfile(const Profile::Ptr & profile)406 void ProfileManager::setDefaultProfile(const Profile::Ptr &profile)
407 {
408 Q_ASSERT(findProfile(profile) != _profiles.cend());
409
410 const auto oldDefault = _defaultProfile;
411 _defaultProfile = profile;
412 ProfileModel::instance()->setDefault(profile);
413
414 saveDefaultProfile();
415
416 // Setting/unsetting a profile as the default is a sort of a
417 // "profile change", useful for updating the icon/font of the
418 // "default profile in e.g. 'File -> New Tab' menu.
419 Q_EMIT profileChanged(oldDefault);
420 Q_EMIT profileChanged(profile);
421 }
422
saveDefaultProfile()423 void ProfileManager::saveDefaultProfile()
424 {
425 QString path = _defaultProfile->path();
426 ProfileWriter writer;
427
428 if (path.isEmpty()) {
429 path = writer.getPath(_defaultProfile);
430 }
431
432 KConfigGroup group = m_config->group("Desktop Entry");
433 group.writeEntry("DefaultProfile", QUrl::fromLocalFile(path).fileName());
434 m_config->sync();
435 }
436
loadShortcuts()437 void ProfileManager::loadShortcuts()
438 {
439 KConfigGroup shortcutGroup = m_config->group("Profile Shortcuts");
440
441 const QLatin1String suffix(".profile");
442 auto findByName = [this, suffix](const QString &name) {
443 return std::find_if(_profiles.cbegin(), _profiles.cend(), [&name, suffix](const Profile::Ptr &p) {
444 return p->name() == name //
445 || (p->name() + suffix) == name; // For backwards compatibility
446 });
447 };
448
449 const QMap<QString, QString> entries = shortcutGroup.entryMap();
450 for (auto it = entries.cbegin(), endIt = entries.cend(); it != endIt; ++it) {
451 auto profileIt = findByName(it.value());
452 if (profileIt == _profiles.cend()) {
453 continue;
454 }
455
456 _shortcuts.push_back({*profileIt, QKeySequence::fromString(it.key())});
457 }
458 }
459
saveShortcuts()460 void ProfileManager::saveShortcuts()
461 {
462 if (_profileShortcutsChanged) {
463 _profileShortcutsChanged = false;
464
465 KConfigGroup shortcutGroup = m_config->group("Profile Shortcuts");
466 shortcutGroup.deleteGroup();
467
468 for (const auto &[profile, keySeq] : _shortcuts) {
469 shortcutGroup.writeEntry(keySeq.toString(), profile->name());
470 }
471
472 m_config->sync();
473 }
474 }
475
setShortcut(Profile::Ptr profile,const QKeySequence & keySequence)476 void ProfileManager::setShortcut(Profile::Ptr profile, const QKeySequence &keySequence)
477 {
478 _profileShortcutsChanged = true;
479 QKeySequence existingShortcut = shortcut(profile);
480
481 auto profileIt = std::find_if(_shortcuts.begin(), _shortcuts.end(), [&profile](const ShortcutData &data) {
482 return data.profileKey == profile;
483 });
484 if (profileIt != _shortcuts.end()) {
485 // There is a previous shortcut for this profile, replace it with the new one
486 profileIt->keySeq = keySequence;
487 Q_EMIT shortcutChanged(profileIt->profileKey, profileIt->keySeq);
488 } else {
489 // No previous shortcut for this profile
490 const ShortcutData &newData = _shortcuts.emplace_back(ShortcutData{profile, keySequence});
491 Q_EMIT shortcutChanged(newData.profileKey, newData.keySeq);
492 }
493
494 auto keySeqIt = std::find_if(_shortcuts.begin(), _shortcuts.end(), [&keySequence, &profile](const ShortcutData &data) {
495 return data.profileKey != profile && data.keySeq == keySequence;
496 });
497 if (keySeqIt != _shortcuts.end()) {
498 // There is a profile with shortcut "keySequence" which has been
499 // associated with another profile >>> unset it
500 Q_EMIT shortcutChanged(keySeqIt->profileKey, {});
501 _shortcuts.erase(keySeqIt);
502 }
503 }
504
shortcut(Profile::Ptr profile) const505 QKeySequence ProfileManager::shortcut(Profile::Ptr profile) const
506 {
507 auto it = std::find_if(_shortcuts.cbegin(), _shortcuts.cend(), [&profile](const ShortcutData &data) {
508 return data.profileKey == profile;
509 });
510
511 return it != _shortcuts.cend() ? it->keySeq : QKeySequence{};
512 }
513