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