/* This source file is part of Konsole, a terminal emulator. Copyright 2006-2008 by Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ // Own #include "SessionManager.h" // Qt #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include // Konsole #include "Session.h" #include "History.h" using namespace Konsole; static bool profileIndexLessThan(const Profile::Ptr& p1, const Profile::Ptr& p2) { return p1->menuIndexAsInt() <= p2->menuIndexAsInt(); } static bool profileNameLessThan(const Profile::Ptr& p1, const Profile::Ptr& p2) { return QString::localeAwareCompare(p1->name(), p2->name()) <= 0; } static void sortByIndexProfileList(QList& list) { qStableSort(list.begin(), list.end(), profileIndexLessThan); } static void sortByNameProfileList(QList& list) { qStableSort(list.begin(), list.end(), profileNameLessThan); } SessionManager::SessionManager() : _loadedAllProfiles(false) , _loadedFavorites(false) { //map finished() signals from sessions _sessionMapper = new QSignalMapper(this); connect(_sessionMapper , SIGNAL(mapped(QObject*)) , this , SLOT(sessionTerminated(QObject*))); //load fallback profile _fallbackProfile = Profile::Ptr(new FallbackProfile); addProfile(_fallbackProfile); //locate and load default profile KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig("konsolerc"); const KConfigGroup group = konsoleConfig->group("Desktop Entry"); const QString defaultProfileFileName = group.readEntry("DefaultProfile", "Shell.profile"); const QString path = KStandardDirs::locate("data", "konsole/" + defaultProfileFileName); if (!path.isEmpty()) { Profile::Ptr profile = loadProfile(path); if (profile) _defaultProfile = profile; } Q_ASSERT(_profiles.count() > 0); Q_ASSERT(_defaultProfile); // get shortcuts and paths of profiles associated with // them - this doesn't load the shortcuts themselves, // that is done on-demand. loadShortcuts(); } SessionManager::~SessionManager() { if (_sessions.count() > 0) { kWarning() << "Konsole SessionManager destroyed with sessions still alive"; // ensure that the Session doesn't later try to call back and do things to the // SessionManager foreach(Session* session , _sessions) { disconnect(session , 0 , this , 0); } } } K_GLOBAL_STATIC(SessionManager , theSessionManager) SessionManager* SessionManager::instance() { return theSessionManager; } Profile::Ptr SessionManager::loadProfile(const QString& shortPath) { // the fallback profile has a 'special' path name, "FALLBACK/" if (shortPath == _fallbackProfile->property(Profile::Path)) return _fallbackProfile; QString path = shortPath; // add a suggested suffix and relative prefix if missing QFileInfo fileInfo(path); if (fileInfo.isDir()) return Profile::Ptr(); if (fileInfo.suffix() != "profile") path.append(".profile"); if (fileInfo.path().isEmpty() || fileInfo.path() == ".") path.prepend(QString("konsole") + QDir::separator()); // if the file is not an absolute path, look it up if (!fileInfo.isAbsolute()) path = KStandardDirs::locate("data", path); // if the file is not found, return imediately if ( path.isEmpty() ) { return Profile::Ptr(); } // check that we have not already loaded this profile foreach ( const Profile::Ptr& profile, _profiles) { if (profile->path() == path) return profile; } // guard to prevent problems if a profile specifies itself as its parent // or if there is recursion in the "inheritance" chain // (eg. two profiles, A and B, specifying each other as their parents) static QStack recursionGuard; PopStackOnExit popGuardOnExit(recursionGuard); if (recursionGuard.contains(path)) { kWarning() << "Ignoring attempt to load profile recursively from" << path; return _fallbackProfile; } else recursionGuard.push(path); // load the profile ProfileReader* reader = new KDE4ProfileReader; Profile::Ptr newProfile = Profile::Ptr(new Profile(fallbackProfile())); newProfile->setProperty(Profile::Path, path); QString parentProfilePath; bool result = reader->readProfile(path, newProfile, parentProfilePath); if (!parentProfilePath.isEmpty()) { Profile::Ptr parentProfile = loadProfile(parentProfilePath); newProfile->setParent(parentProfile); } delete reader; if (!result) { kWarning() << "Could not load profile from " << path; return Profile::Ptr(); } else { addProfile(newProfile); return newProfile; } } QStringList SessionManager::availableProfilePaths() const { KDE4ProfileReader kde4Reader; QStringList profiles; profiles += kde4Reader.findProfiles(); return profiles; } void SessionManager::loadAllProfiles() { if (_loadedAllProfiles) return; const QStringList& paths = availableProfilePaths(); foreach ( const QString& path, paths) { loadProfile(path); } _loadedAllProfiles = true; } void SessionManager::sortProfiles(QList& list) { QList lackingIndices; QList havingIndices; for (int i = 0; i < list.size(); ++i) { // dis-regard the fallback profile if (list.at(i)->path() == _fallbackProfile->property(Profile::Path)) continue; if (list.at(i)->menuIndexAsInt() == 0) lackingIndices.append(list.at(i)); else havingIndices.append(list.at(i)); } // sort by index sortByIndexProfileList(havingIndices); // sort alphabetically those w/o an index sortByNameProfileList(lackingIndices); // Put those with indices in sequential order w/o any gaps int i = 0; for (i = 0; i < havingIndices.size(); ++i) { Profile::Ptr tempProfile = havingIndices.at(i); tempProfile->setProperty(Profile::MenuIndex, QString::number(i + 1)); havingIndices.replace(i, tempProfile); } // Put those w/o indices in sequential order for (int j = 0; j < lackingIndices.size(); ++j) { Profile::Ptr tempProfile = lackingIndices.at(j); tempProfile->setProperty(Profile::MenuIndex, QString::number(j + 1 + i)); lackingIndices.replace(j, tempProfile); } // combine the 2 list: first those who had indices list.clear(); list.append(havingIndices); list.append(lackingIndices); } void SessionManager::saveSettings() { // save default profile saveDefaultProfile(); // save shortcuts saveShortcuts(); // save favorites saveFavorites(); // ensure shortcuts/favorites settings are synced into disk KSharedConfigPtr appConfig = KGlobal::config(); appConfig->sync(); // FIXME: another workaround for using 'DefaultProfile' entry in 'konsolerc' KSharedConfigPtr konsoleConfig = KSharedConfig::openConfig("konsolerc"); konsoleConfig->sync(); } void SessionManager::closeAllSessions() { // close remaining sessions foreach(Session * session , _sessions) { session->close(); } _sessions.clear(); } const QList SessionManager::sessions() const { return _sessions; } void SessionManager::updateSession(Session* session) { Profile::Ptr profile = _sessionProfiles[session]; // Temp fix for crashes when changing profiles 256357, 246054 if (!profile) profile = defaultProfile(); Q_ASSERT(profile); applyProfile(session, profile, false); emit sessionUpdated(session); } Session* SessionManager::createSession(Profile::Ptr profile) { if (!profile) profile = defaultProfile(); if (!_profiles.contains(profile)) addProfile(profile); //configuration information found, create a new session based on this Session* session = new Session(); Q_ASSERT(session); applyProfile(session, profile, false); connect(session , SIGNAL(profileChangeCommandReceived(QString)) , this , SLOT(sessionProfileCommandReceived(QString))); //ask for notification when session dies _sessionMapper->setMapping(session, session); connect(session , SIGNAL(finished()) , _sessionMapper , SLOT(map())); //add session to active list _sessions << session; _sessionProfiles.insert(session, profile); return session; } void SessionManager::sessionTerminated(QObject* sessionObject) { Session* session = qobject_cast(sessionObject); Q_ASSERT(session); _sessions.removeAll(session); _sessionProfiles.remove(session); _sessionRuntimeProfiles.remove(session); session->deleteLater(); } QList SessionManager::sortedFavorites() { QList favorites = findFavorites().toList(); sortProfiles(favorites); return favorites; } QList SessionManager::allProfiles() { loadAllProfiles(); return _profiles.toList(); } Profile::Ptr SessionManager::defaultProfile() const { return _defaultProfile; } Profile::Ptr SessionManager::fallbackProfile() const { return _fallbackProfile; } QString SessionManager::saveProfile(Profile::Ptr profile) { ProfileWriter* writer = new KDE4ProfileWriter; QString newPath = writer->getPath(profile); writer->writeProfile(newPath, profile); delete writer; return newPath; } void SessionManager::changeProfile(Profile::Ptr profile, QHash propertyMap, bool persistent) { Q_ASSERT(profile); // insert the changes into the existing Profile instance QListIterator iter(propertyMap.keys()); while (iter.hasNext()) { const Profile::Property property = iter.next(); profile->setProperty(property, propertyMap[property]); } // never save a profile with empty name into disk! persistent = persistent && !profile->name().isEmpty() ; // when changing a group, iterate through the profiles // in the group and call changeProfile() on each of them // // this is so that each profile in the group, the profile is // applied, a change notification is emitted and the profile // is saved to disk ProfileGroup::Ptr group = profile->asGroup(); if (group) { foreach(const Profile::Ptr & profile, group->profiles()) { changeProfile(profile, propertyMap, persistent); } return; } // apply the changes to existing sessions applyProfile(profile, true); // notify the world about the change emit profileChanged(profile); // save changes to disk, unless the profile is hidden, in which case // it has no file on disk if (persistent && !profile->isHidden()) { profile->setProperty(Profile::Path, saveProfile(profile)); } } void SessionManager::applyProfile(Profile::Ptr profile , bool modifiedPropertiesOnly) { foreach ( Session* session, _sessions ) { if (_sessionProfiles[session] == profile) applyProfile(session, profile, modifiedPropertiesOnly); } } Profile::Ptr SessionManager::sessionProfile(Session* session) const { return _sessionProfiles[session]; } void SessionManager::setSessionProfile(Session* session, Profile::Ptr profile) { _sessionProfiles[session] = profile; updateSession(session); } void SessionManager::applyProfile(Session* session, const Profile::Ptr profile , bool modifiedPropertiesOnly) { Q_ASSERT(profile); _sessionProfiles[session] = profile; ShouldApplyProperty apply(profile, modifiedPropertiesOnly); // Basic session settings if (apply.shouldApply(Profile::Name)) session->setTitle(Session::NameRole, profile->name()); if (apply.shouldApply(Profile::Command)) session->setProgram(profile->command()); if (apply.shouldApply(Profile::Arguments)) session->setArguments(profile->arguments()); if (apply.shouldApply(Profile::Directory)) session->setInitialWorkingDirectory(profile->defaultWorkingDirectory()); if (apply.shouldApply(Profile::Environment)) { // add environment variable containing home directory of current profile // (if specified) QStringList environment = profile->property(Profile::Environment); environment << QString("PROFILEHOME=%1").arg(profile->defaultWorkingDirectory()); environment << QString("KONSOLE_PROFILE_NAME=%1").arg(profile->name()); session->setEnvironment(environment); } if (apply.shouldApply(Profile::Icon)) session->setIconName(profile->icon()); // Key bindings if (apply.shouldApply(Profile::KeyBindings)) session->setKeyBindings(profile->property(Profile::KeyBindings)); // Tab formats if (apply.shouldApply(Profile::LocalTabTitleFormat)) session->setTabTitleFormat(Session::LocalTabTitle , profile->property(Profile::LocalTabTitleFormat)); if (apply.shouldApply(Profile::RemoteTabTitleFormat)) session->setTabTitleFormat(Session::RemoteTabTitle , profile->property(Profile::RemoteTabTitleFormat)); // History if (apply.shouldApply(Profile::HistoryMode) || apply.shouldApply(Profile::HistorySize)) { int mode = profile->property(Profile::HistoryMode); switch ((Profile::HistoryModeEnum)mode) { case Profile::DisableHistory: session->setHistoryType(HistoryTypeNone()); break; case Profile::FixedSizeHistory: { int lines = profile->property(Profile::HistorySize); session->setHistoryType(CompactHistoryType(lines)); } break; case Profile::UnlimitedHistory: session->setHistoryType(HistoryTypeFile()); break; } } // Terminal features if (apply.shouldApply(Profile::FlowControlEnabled)) session->setFlowControlEnabled(profile->property(Profile::FlowControlEnabled)); // Encoding if (apply.shouldApply(Profile::DefaultEncoding)) { QByteArray name = profile->property(Profile::DefaultEncoding).toUtf8(); session->setCodec(QTextCodec::codecForName(name)); } // Monitor Silence if (apply.shouldApply(Profile::SilenceSeconds)) session->setMonitorSilenceSeconds(profile->property(Profile::SilenceSeconds)); } void SessionManager::addProfile(Profile::Ptr profile) { if (_profiles.isEmpty()) _defaultProfile = profile; _profiles.insert(profile); emit profileAdded(profile); } bool SessionManager::deleteProfile(Profile::Ptr profile) { bool wasDefault = (profile == defaultProfile()); if (profile) { // try to delete the config file if (profile->isPropertySet(Profile::Path) && QFile::exists(profile->path())) { if (!QFile::remove(profile->path())) { kWarning() << "Could not delete profile: " << profile->path() << "The file is most likely in a directory which is read-only."; return false; } } // remove from favorites, profile list, shortcut list etc. setFavorite(profile, false); setShortcut(profile, QKeySequence()); _profiles.remove(profile); // mark the profile as hidden so that it does not show up in the // Manage Profiles dialog and is not saved to disk profile->setHidden(true); } // if we just deleted the default profile, // replace it with a random profile from the list if (wasDefault) { setDefaultProfile(_profiles.toList().first()); } emit profileRemoved(profile); return true; } void SessionManager::setDefaultProfile(Profile::Ptr profile) { Q_ASSERT(_profiles.contains(profile)); _defaultProfile = profile; } void SessionManager::saveDefaultProfile() { QString path = _defaultProfile->path(); if (path.isEmpty()) path = KDE4ProfileWriter().getPath(_defaultProfile); QFileInfo fileInfo(path); // TODO: writing to konsolerc is a temporary workaround KSharedConfigPtr config = KSharedConfig::openConfig("konsolerc"); KConfigGroup group = config->group("Desktop Entry"); group.writeEntry("DefaultProfile", fileInfo.fileName()); } QSet SessionManager::findFavorites() { loadFavorites(); return _favorites; } void SessionManager::setFavorite(Profile::Ptr profile , bool favorite) { if (!_profiles.contains(profile)) addProfile(profile); if (favorite && !_favorites.contains(profile)) { _favorites.insert(profile); emit favoriteStatusChanged(profile, favorite); } else if (!favorite && _favorites.contains(profile)) { _favorites.remove(profile); emit favoriteStatusChanged(profile, favorite); } } void SessionManager::loadShortcuts() { KSharedConfigPtr appConfig = KGlobal::config(); KConfigGroup shortcutGroup = appConfig->group("Profile Shortcuts"); QMap entries = shortcutGroup.entryMap(); QMapIterator iter(entries); while (iter.hasNext()) { iter.next(); QKeySequence shortcut = QKeySequence::fromString(iter.key()); QString profilePath = iter.value(); ShortcutData data; // if the file is not an absolute path, look it up QFileInfo fileInfo(profilePath); if (!fileInfo.isAbsolute()) { profilePath = KStandardDirs::locate("data","konsole/" + profilePath); } data.profilePath = profilePath; _shortcuts.insert(shortcut, data); } } void SessionManager::saveShortcuts() { KSharedConfigPtr appConfig = KGlobal::config(); KConfigGroup shortcutGroup = appConfig->group("Profile Shortcuts"); shortcutGroup.deleteGroup(); QMapIterator iter(_shortcuts); while (iter.hasNext()) { iter.next(); QString shortcutString = iter.key().toString(); // if the profile path in "Profile Shortcuts" is an absolute path, // take the profile name QFileInfo fileInfo(iter.value().profilePath); QString profileName; if (fileInfo.isAbsolute()) { profileName = fileInfo.fileName(); } else { profileName = iter.value().profilePath; } shortcutGroup.writeEntry(shortcutString, profileName); } } void SessionManager::setShortcut(Profile::Ptr profile , const QKeySequence& keySequence) { QKeySequence existingShortcut = shortcut(profile); _shortcuts.remove(existingShortcut); if (keySequence.isEmpty()) return; ShortcutData data; data.profileKey = profile; data.profilePath = profile->path(); // TODO - This won't work if the profile doesn't // have a path yet _shortcuts.insert(keySequence, data); emit shortcutChanged(profile, keySequence); } void SessionManager::loadFavorites() { if ( _loadedFavorites ) return ; KSharedConfigPtr appConfig = KGlobal::config(); KConfigGroup favoriteGroup = appConfig->group("Favorite Profiles"); QSet favoriteSet; if (favoriteGroup.hasKey("Favorites")) { QStringList list = favoriteGroup.readEntry("Favorites", QStringList()); favoriteSet = QSet::fromList(list); } else { // if there is no favorites key at all, mark the // supplied 'Shell.profile' as the only favorite favoriteSet << "Shell.profile"; } // look for favorites amongst those already loaded foreach ( const Profile::Ptr& profile, _profiles ) { const QString& path = profile->path(); if (favoriteSet.contains(path)) { _favorites.insert(profile); favoriteSet.remove(path); } } // load any remaining favorites foreach ( const QString& favorite, favoriteSet ) { Profile::Ptr profile = loadProfile(favorite); if (profile) _favorites.insert(profile); } _loadedFavorites = true; } void SessionManager::saveFavorites() { KSharedConfigPtr appConfig = KGlobal::config(); KConfigGroup favoriteGroup = appConfig->group("Favorite Profiles"); QStringList paths; foreach ( const Profile::Ptr& profile, _favorites ) { Q_ASSERT(_profiles.contains(profile) && profile); QFileInfo fileInfo(profile->path()); QString profileName; if (fileInfo.isAbsolute()) { profileName = fileInfo.fileName(); } else { profileName = profile->path(); } paths << profileName; } favoriteGroup.writeEntry("Favorites", paths); } QList SessionManager::shortcuts() { return _shortcuts.keys(); } Profile::Ptr SessionManager::findByShortcut(const QKeySequence& shortcut) { Q_ASSERT(_shortcuts.contains(shortcut)); if (!_shortcuts[shortcut].profileKey) { Profile::Ptr key = loadProfile(_shortcuts[shortcut].profilePath); if (!key) { _shortcuts.remove(shortcut); return Profile::Ptr(); } _shortcuts[shortcut].profileKey = key; } return _shortcuts[shortcut].profileKey; } void SessionManager::sessionProfileCommandReceived(const QString& text) { Session* session = qobject_cast(sender()); Q_ASSERT(session); ProfileCommandParser parser; QHash changes = parser.parse(text); Profile::Ptr newProfile; if (!_sessionRuntimeProfiles.contains(session)) { newProfile = new Profile(_sessionProfiles[session]); _sessionRuntimeProfiles.insert(session, newProfile); } else { newProfile = _sessionRuntimeProfiles[session]; } QHashIterator iter(changes); while (iter.hasNext()) { iter.next(); newProfile->setProperty(iter.key(), iter.value()); } _sessionProfiles[session] = newProfile; applyProfile(newProfile, true); emit sessionUpdated(session); } QKeySequence SessionManager::shortcut(Profile::Ptr profile) const { QMapIterator iter(_shortcuts); while (iter.hasNext()) { iter.next(); if (iter.value().profileKey == profile || iter.value().profilePath == profile->path()) return iter.key(); } return QKeySequence(); } void SessionManager::saveSessions(KConfig* config) { // The session IDs can't be restored. // So we need to map the old ID to the future new ID. int n = 1; _restoreMapping.clear(); foreach(Session * session, _sessions) { QString name = QLatin1String("Session") + QString::number(n); KConfigGroup group(config, name); group.writePathEntry("Profile", _sessionProfiles.value(session)->path()); session->saveSession(group); _restoreMapping.insert(session, n); n++; } KConfigGroup group(config, "Number"); group.writeEntry("NumberOfSessions", _sessions.count()); } int SessionManager::getRestoreId(Session* session) { return _restoreMapping.value(session); } void SessionManager::restoreSessions(KConfig* config) { KConfigGroup group(config, "Number"); int sessions; // Any sessions saved? if ((sessions = group.readEntry("NumberOfSessions", 0)) > 0) { for (int n = 1; n <= sessions; n++) { QString name = QLatin1String("Session") + QString::number(n); KConfigGroup sessionGroup(config, name); QString profile = sessionGroup.readPathEntry("Profile", QString()); Profile::Ptr ptr = defaultProfile(); if (!profile.isEmpty()) ptr = loadProfile(profile); Session* session = createSession(ptr); session->restoreSession(sessionGroup); } } } Session* SessionManager::idToSession(int id) { Q_ASSERT(id); foreach(Session * session, _sessions) { if (session->sessionId() == id) return session; } // this should not happen Q_ASSERT(0); return 0; } SessionListModel::SessionListModel(QObject* parent) : QAbstractListModel(parent) { } void SessionListModel::setSessions(const QList& sessions) { _sessions = sessions; foreach(Session * session, sessions) { connect(session, SIGNAL(finished()), this, SLOT(sessionFinished())); } reset(); } QVariant SessionListModel::data(const QModelIndex& index, int role) const { Q_ASSERT(index.isValid()); int row = index.row(); int column = index.column(); Q_ASSERT(row >= 0 && row < _sessions.count()); Q_ASSERT(column >= 0 && column < 2); switch (role) { case Qt::DisplayRole: if (column == 1) return _sessions[row]->title(Session::DisplayedTitleRole); else if (column == 0) return _sessions[row]->sessionId(); break; case Qt::DecorationRole: if (column == 1) return KIcon(_sessions[row]->iconName()); else return QVariant(); } return QVariant(); } QVariant SessionListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); if (orientation == Qt::Vertical) return QVariant(); else { switch (section) { case 0: return i18nc("@item:intable The session index", "Number"); case 1: return i18nc("@item:intable The session title", "Title"); default: return QVariant(); } } } int SessionListModel::columnCount(const QModelIndex&) const { return 2; } int SessionListModel::rowCount(const QModelIndex&) const { return _sessions.count(); } QModelIndex SessionListModel::parent(const QModelIndex&) const { return QModelIndex(); } void SessionListModel::sessionFinished() { Session* session = qobject_cast(sender()); int row = _sessions.indexOf(session); if (row != -1) { beginRemoveRows(QModelIndex(), row, row); sessionRemoved(session); _sessions.removeAt(row); endRemoveRows(); } } QModelIndex SessionListModel::index(int row, int column, const QModelIndex& parent) const { if (hasIndex(row, column, parent)) return createIndex(row, column, _sessions[row]); else return QModelIndex(); } #include "SessionManager.moc"