File indexing completed on 2024-04-28 05:50:45

0001 /*
0002     This source file is part of Konsole, a terminal emulator.
0003 
0004     SPDX-FileCopyrightText: 2006-2008 Robert Knight <robertknight@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 // Own
0010 #include "ProfileManager.h"
0011 #include "PopStackOnExit.h"
0012 
0013 // Qt
0014 #include <QDir>
0015 #include <QFileInfo>
0016 #include <QString>
0017 #include <QUrl>
0018 
0019 // KDE
0020 #include <KConfig>
0021 #include <KConfigGroup>
0022 #include <KLocalizedString>
0023 #include <KMessageBox>
0024 
0025 // Konsole
0026 #include "ProfileGroup.h"
0027 #include "ProfileModel.h"
0028 #include "ProfileReader.h"
0029 #include "ProfileWriter.h"
0030 
0031 Q_LOGGING_CATEGORY(KonsoleProfileDebug, "org.kde.konsole.profile", QtDebugMsg)
0032 
0033 using namespace Konsole;
0034 
0035 static bool stringLessThan(const QString &p1, const QString &p2)
0036 {
0037     return QString::localeAwareCompare(p1, p2) < 0;
0038 }
0039 
0040 static bool profileNameLessThan(const Profile::Ptr &p1, const Profile::Ptr &p2)
0041 {
0042     // Always put the built-in profile at the top
0043     if (p1->isBuiltin()) {
0044         return true;
0045     } else if (p2->isBuiltin()) {
0046         return false;
0047     }
0048 
0049     return stringLessThan(p1->name(), p2->name());
0050 }
0051 
0052 ProfileManager::ProfileManager()
0053     : m_config(KSharedConfig::openConfig())
0054 {
0055     // ensure built-in profile is available
0056     initBuiltinProfile();
0057     _defaultProfile = _builtinProfile;
0058 
0059     // lookup the default profile specified in <App>rc
0060     // For stand-alone Konsole, m_config is just "konsolerc"
0061     // For konsolepart, m_config might be "yakuakerc", "dolphinrc", "katerc"...
0062     KConfigGroup group = m_config->group(QStringLiteral("Desktop Entry"));
0063     QString defaultProfileFileName = group.readEntry("DefaultProfile", "");
0064 
0065     // if the hosting application of konsolepart does not specify its own
0066     // default profile, use the default profile of stand-alone Konsole.
0067     if (defaultProfileFileName.isEmpty()) {
0068         KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig(QStringLiteral("konsolerc"));
0069         group = konsoleConfig->group(QStringLiteral("Desktop Entry"));
0070         defaultProfileFileName = group.readEntry("DefaultProfile", "");
0071     }
0072 
0073     loadAllProfiles(defaultProfileFileName);
0074     loadShortcuts();
0075 
0076     Q_ASSERT(_profiles.size() > 0);
0077     Q_ASSERT(_defaultProfile);
0078 }
0079 
0080 ProfileManager::~ProfileManager() = default;
0081 
0082 Q_GLOBAL_STATIC(ProfileManager, theProfileManager)
0083 ProfileManager *ProfileManager::instance()
0084 {
0085     return theProfileManager;
0086 }
0087 
0088 ProfileManager::Iterator ProfileManager::findProfile(const Profile::Ptr &profile) const
0089 {
0090     return std::find(_profiles.cbegin(), _profiles.cend(), profile);
0091 }
0092 
0093 void ProfileManager::initBuiltinProfile()
0094 {
0095     _builtinProfile = Profile::Ptr(new Profile());
0096     _builtinProfile->useBuiltin();
0097     addProfile(_builtinProfile);
0098 }
0099 
0100 Profile::Ptr ProfileManager::loadProfile(const QString &shortPath)
0101 {
0102     if (shortPath == builtinProfile()->path()) {
0103         return builtinProfile();
0104     }
0105 
0106     QString path = shortPath;
0107 
0108     // add a suggested suffix and relative prefix if missing
0109     QFileInfo fileInfo(path);
0110 
0111     if (fileInfo.isDir()) {
0112         return Profile::Ptr();
0113     }
0114 
0115     if (fileInfo.suffix() != QLatin1String("profile")) {
0116         path.append(QLatin1String(".profile"));
0117     }
0118     if (fileInfo.path().isEmpty() || fileInfo.path() == QLatin1String(".")) {
0119         path.prepend(QLatin1String("konsole") + QDir::separator());
0120     }
0121 
0122     // if the file is not an absolute path, look it up
0123     if (!fileInfo.isAbsolute()) {
0124         path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, path);
0125     }
0126 
0127     // if the file is not found, return immediately
0128     if (path.isEmpty()) {
0129         return Profile::Ptr();
0130     }
0131 
0132     // check that we have not already loaded this profile
0133     for (const Profile::Ptr &profile : _profiles) {
0134         if (profile->path() == path) {
0135             return profile;
0136         }
0137     }
0138 
0139     // guard to prevent problems if a profile specifies itself as its parent
0140     // or if there is recursion in the "inheritance" chain
0141     // (eg. two profiles, A and B, specifying each other as their parents)
0142     static QStack<QString> recursionGuard;
0143     PopStackOnExit<QString> popGuardOnExit(recursionGuard);
0144 
0145     if (recursionGuard.contains(path)) {
0146         qCDebug(KonsoleProfileDebug) << "Ignoring attempt to load profile recursively from" << path;
0147         return _builtinProfile;
0148     }
0149     recursionGuard.push(path);
0150 
0151     // load the profile
0152     ProfileReader reader;
0153 
0154     Profile::Ptr newProfile = Profile::Ptr(new Profile(builtinProfile()));
0155     newProfile->setProperty(Profile::Path, path);
0156 
0157     QString parentProfilePath;
0158     bool result = reader.readProfile(path, newProfile, parentProfilePath);
0159 
0160     if (!parentProfilePath.isEmpty()) {
0161         Profile::Ptr parentProfile = loadProfile(parentProfilePath);
0162         // To avoid crashes make sure to NOT set an invalid parent
0163         if (parentProfile == nullptr) {
0164             qCDebug(KonsoleProfileDebug) << "Profile " << newProfile->name() << " has an invalid parent " << parentProfilePath;
0165         } else {
0166             newProfile->setParent(parentProfile);
0167         }
0168     }
0169 
0170     if (!result) {
0171         qCDebug(KonsoleProfileDebug) << "Could not load profile from " << path;
0172         return Profile::Ptr();
0173     } else if (newProfile->name().isEmpty()) {
0174         qCWarning(KonsoleProfileDebug) << path << " does not have a valid name, ignoring.";
0175         return Profile::Ptr();
0176     } else {
0177         addProfile(newProfile);
0178         return newProfile;
0179     }
0180 }
0181 QStringList ProfileManager::availableProfilePaths() const
0182 {
0183     ProfileReader reader;
0184 
0185     QStringList paths;
0186     paths += reader.findProfiles();
0187 
0188     std::stable_sort(paths.begin(), paths.end(), stringLessThan);
0189 
0190     return paths;
0191 }
0192 
0193 QStringList ProfileManager::availableProfileNames() const
0194 {
0195     QStringList names;
0196 
0197     const QList<Profile::Ptr> allProfiles = ProfileManager::instance()->allProfiles();
0198     for (const Profile::Ptr &profile : allProfiles) {
0199         if (!profile->isHidden()) {
0200             names.push_back(profile->name());
0201         }
0202     }
0203 
0204     std::stable_sort(names.begin(), names.end(), stringLessThan);
0205 
0206     return names;
0207 }
0208 
0209 void ProfileManager::loadAllProfiles(const QString &defaultProfileFileName)
0210 {
0211     const QStringList &paths = availableProfilePaths();
0212     for (const QString &path : paths) {
0213         Profile::Ptr profile = loadProfile(path);
0214         if (profile && !defaultProfileFileName.isEmpty() && path.endsWith(QLatin1Char('/') + defaultProfileFileName)) {
0215             _defaultProfile = profile;
0216         }
0217     }
0218 }
0219 
0220 void ProfileManager::saveSettings()
0221 {
0222     saveShortcuts();
0223 }
0224 
0225 void ProfileManager::sortProfiles()
0226 {
0227     std::sort(_profiles.begin(), _profiles.end(), profileNameLessThan);
0228 }
0229 
0230 QList<Profile::Ptr> ProfileManager::allProfiles()
0231 {
0232     sortProfiles();
0233     return loadedProfiles();
0234 }
0235 
0236 QList<Profile::Ptr> ProfileManager::loadedProfiles() const
0237 {
0238     return {_profiles.cbegin(), _profiles.cend()};
0239 }
0240 
0241 Profile::Ptr ProfileManager::defaultProfile() const
0242 {
0243     return _defaultProfile;
0244 }
0245 Profile::Ptr ProfileManager::builtinProfile() const
0246 {
0247     return _builtinProfile;
0248 }
0249 
0250 QString ProfileManager::generateUniqueName() const
0251 {
0252     const QStringList existingProfileNames = availableProfileNames();
0253     int nameSuffix = 1;
0254     QString uniqueProfileName;
0255     do {
0256         uniqueProfileName = i18nc("New profile name, %1 is a number", "Profile %1", QString::number(nameSuffix));
0257         ++nameSuffix;
0258     } while (existingProfileNames.contains(uniqueProfileName));
0259 
0260     return uniqueProfileName;
0261 }
0262 
0263 QString ProfileManager::saveProfile(const Profile::Ptr &profile)
0264 {
0265     ProfileWriter writer;
0266 
0267     QString newPath = writer.getPath(profile);
0268 
0269     if (!writer.writeProfile(newPath, profile)) {
0270         KMessageBox::error(nullptr, i18n("Konsole does not have permission to save this profile to %1", newPath));
0271     }
0272 
0273     return newPath;
0274 }
0275 
0276 void ProfileManager::changeProfile(Profile::Ptr profile, const Profile::PropertyMap &propertyMap, bool persistent)
0277 {
0278     Q_ASSERT(profile);
0279 
0280     const QString origPath = profile->path();
0281     const QKeySequence origShortcut = shortcut(profile);
0282     const bool isDefaultProfile = profile == defaultProfile();
0283 
0284     // Don't save a profile with an empty name on disk
0285     persistent = persistent && !profile->name().isEmpty();
0286 
0287     // Insert the changes into the existing Profile instance
0288     profile->assignProperties(propertyMap);
0289 
0290     const bool isRenamed = std::any_of(propertyMap.cbegin(), propertyMap.cend(), [](const auto &pair) {
0291         Profile::Property prop = pair.first;
0292         return prop == Profile::Name || prop == Profile::UntranslatedName;
0293     });
0294 
0295     // when changing a group, iterate through the profiles
0296     // in the group and call changeProfile() on each of them
0297     //
0298     // this is so that each profile in the group, the profile is
0299     // applied, a change notification is emitted and the profile
0300     // is saved to disk
0301     ProfileGroup::Ptr group = profile->asGroup();
0302     if (group) {
0303         const QList<Profile::Ptr> profiles = group->profiles();
0304         for (const Profile::Ptr &groupProfile : profiles) {
0305             changeProfile(groupProfile, propertyMap, persistent);
0306         }
0307         return;
0308     }
0309 
0310     // save changes to disk, unless the profile is hidden, in which case
0311     // it has no file on disk
0312     if (persistent && !profile->isHidden()) {
0313         profile->setProperty(Profile::Path, saveProfile(profile));
0314     }
0315 
0316     if (isRenamed) {
0317         // origPath is empty when saving a new profile
0318         if (!origPath.isEmpty()) {
0319             // Delete the old/redundant .profile from disk
0320             QFile::remove(origPath);
0321 
0322             // Change the default profile name to the new one
0323             if (isDefaultProfile) {
0324                 setDefaultProfile(profile);
0325             }
0326 
0327             // If the profile had a shortcut, re-assign it to the profile
0328             if (!origShortcut.isEmpty()) {
0329                 setShortcut(profile, origShortcut);
0330             }
0331         }
0332 
0333         sortProfiles();
0334     }
0335 
0336     // Notify the world about the change
0337     Q_EMIT profileChanged(profile);
0338 }
0339 
0340 void ProfileManager::addProfile(const Profile::Ptr &profile)
0341 {
0342     if (_profiles.empty()) {
0343         _defaultProfile = profile;
0344     }
0345 
0346     if (findProfile(profile) == _profiles.cend()) {
0347         _profiles.push_back(profile);
0348         Q_EMIT profileAdded(profile);
0349     }
0350 }
0351 
0352 bool ProfileManager::deleteProfile(Profile::Ptr profile)
0353 {
0354     bool wasDefault = (profile == defaultProfile());
0355 
0356     if (profile) {
0357         // try to delete the config file
0358         if (profile->isPropertySet(Profile::Path) && QFile::exists(profile->path())) {
0359             if (!QFile::remove(profile->path())) {
0360                 qCDebug(KonsoleProfileDebug) << "Could not delete profile: " << profile->path() << "The file is most likely in a directory which is read-only.";
0361 
0362                 return false;
0363             }
0364         }
0365 
0366         setShortcut(profile, QKeySequence());
0367         if (auto it = findProfile(profile); it != _profiles.end()) {
0368             _profiles.erase(it);
0369         }
0370 
0371         // mark the profile as hidden so that it does not show up in the
0372         // Manage Profiles dialog and is not saved to disk
0373         profile->setHidden(true);
0374     }
0375 
0376     // If we just deleted the default profile, replace it with the first
0377     // profile in the list.
0378     if (wasDefault) {
0379         const QList<Profile::Ptr> existingProfiles = allProfiles();
0380         setDefaultProfile(existingProfiles.at(0));
0381     }
0382 
0383     Q_EMIT profileRemoved(profile);
0384 
0385     return true;
0386 }
0387 
0388 void ProfileManager::setDefaultProfile(const Profile::Ptr &profile)
0389 {
0390     Q_ASSERT(findProfile(profile) != _profiles.cend());
0391 
0392     const auto oldDefault = _defaultProfile;
0393     _defaultProfile = profile;
0394     ProfileModel::instance()->setDefault(profile);
0395 
0396     saveDefaultProfile();
0397 
0398     // Setting/unsetting a profile as the default is a sort of a
0399     // "profile change", useful for updating the icon/font of the
0400     // "default profile in e.g. 'File -> New Tab' menu.
0401     Q_EMIT profileChanged(oldDefault);
0402     Q_EMIT profileChanged(profile);
0403 }
0404 
0405 void ProfileManager::saveDefaultProfile()
0406 {
0407     QString path = _defaultProfile->path();
0408     ProfileWriter writer;
0409 
0410     if (path.isEmpty()) {
0411         path = writer.getPath(_defaultProfile);
0412     }
0413 
0414     KConfigGroup group = m_config->group(QStringLiteral("Desktop Entry"));
0415     group.writeEntry("DefaultProfile", QUrl::fromLocalFile(path).fileName());
0416     m_config->sync();
0417 }
0418 
0419 void ProfileManager::loadShortcuts()
0420 {
0421     KConfigGroup shortcutGroup = m_config->group(QStringLiteral("Profile Shortcuts"));
0422 
0423     const QLatin1String suffix(".profile");
0424     auto findByName = [this, suffix](const QString &name) {
0425         return std::find_if(_profiles.cbegin(), _profiles.cend(), [&name, suffix](const Profile::Ptr &p) {
0426             return p->name() == name //
0427                 || (p->name() + suffix) == name; // For backwards compatibility
0428         });
0429     };
0430 
0431     const QMap<QString, QString> entries = shortcutGroup.entryMap();
0432     for (auto it = entries.cbegin(), endIt = entries.cend(); it != endIt; ++it) {
0433         auto profileIt = findByName(it.value());
0434         if (profileIt == _profiles.cend()) {
0435             continue;
0436         }
0437 
0438         _shortcuts.push_back({*profileIt, QKeySequence::fromString(it.key())});
0439     }
0440 }
0441 
0442 void ProfileManager::saveShortcuts()
0443 {
0444     if (_profileShortcutsChanged) {
0445         _profileShortcutsChanged = false;
0446 
0447         KConfigGroup shortcutGroup = m_config->group(QStringLiteral("Profile Shortcuts"));
0448         shortcutGroup.deleteGroup(QLatin1String());
0449 
0450         for (const auto &[profile, keySeq] : _shortcuts) {
0451             shortcutGroup.writeEntry(keySeq.toString(), profile->name());
0452         }
0453 
0454         m_config->sync();
0455     }
0456 }
0457 
0458 void ProfileManager::setShortcut(Profile::Ptr profile, const QKeySequence &keySequence)
0459 {
0460     _profileShortcutsChanged = true;
0461     QKeySequence existingShortcut = shortcut(profile);
0462 
0463     auto profileIt = std::find_if(_shortcuts.begin(), _shortcuts.end(), [&profile](const ShortcutData &data) {
0464         return data.profileKey == profile;
0465     });
0466     if (profileIt != _shortcuts.end()) {
0467         // There is a previous shortcut for this profile, replace it with the new one
0468         profileIt->keySeq = keySequence;
0469         Q_EMIT shortcutChanged(profileIt->profileKey, profileIt->keySeq);
0470     } else {
0471         // No previous shortcut for this profile
0472         const ShortcutData &newData = _shortcuts.emplace_back(ShortcutData{profile, keySequence});
0473         Q_EMIT shortcutChanged(newData.profileKey, newData.keySeq);
0474     }
0475 
0476     auto keySeqIt = std::find_if(_shortcuts.begin(), _shortcuts.end(), [&keySequence, &profile](const ShortcutData &data) {
0477         return data.profileKey != profile && data.keySeq == keySequence;
0478     });
0479     if (keySeqIt != _shortcuts.end()) {
0480         // There is a profile with shortcut "keySequence" which has been
0481         // associated with another profile >>> unset it
0482         Q_EMIT shortcutChanged(keySeqIt->profileKey, {});
0483         _shortcuts.erase(keySeqIt);
0484     }
0485 }
0486 
0487 QKeySequence ProfileManager::shortcut(Profile::Ptr profile) const
0488 {
0489     auto it = std::find_if(_shortcuts.cbegin(), _shortcuts.cend(), [&profile](const ShortcutData &data) {
0490         return data.profileKey == profile;
0491     });
0492 
0493     return it != _shortcuts.cend() ? it->keySeq : QKeySequence{};
0494 }
0495 
0496 // ProfileTest is sourcing profile/ProfileManager.cpp to access helper methods
0497 // automoc though does not support/detect that scenario,
0498 // so only generates the moc file in the autogen include folder for the normal source build, but not for the test
0499 // TODO: move internal methods into separate sources and bundle that into the test, or statically export methods
0500 // #include "moc_ProfileManager.cpp"