File indexing completed on 2024-05-12 05:47:42
0001 /* 0002 * SPDX-FileCopyrightText: 2007-2010 Peter Penz <peter.penz19@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "terminalpanel.h" 0008 0009 #include <KActionCollection> 0010 #include <KIO/DesktopExecParser> 0011 #include <KIO/StatJob> 0012 #include <KJobWidgets> 0013 #include <KLocalizedString> 0014 #include <KMessageWidget> 0015 #include <KMountPoint> 0016 #include <KParts/ReadOnlyPart> 0017 #include <KPluginFactory> 0018 #include <KProtocolInfo> 0019 #include <KShell> 0020 #include <KXMLGUIBuilder> 0021 #include <KXMLGUIFactory> 0022 #include <kde_terminal_interface.h> 0023 0024 #include <QAction> 0025 #include <QDesktopServices> 0026 #include <QDir> 0027 #include <QShowEvent> 0028 #include <QTimer> 0029 #include <QVBoxLayout> 0030 0031 TerminalPanel::TerminalPanel(QWidget *parent) 0032 : Panel(parent) 0033 , m_clearTerminal(true) 0034 , m_mostLocalUrlJob(nullptr) 0035 , m_layout(nullptr) 0036 , m_terminal(nullptr) 0037 , m_terminalWidget(nullptr) 0038 , m_konsolePartMissingMessage(nullptr) 0039 , m_konsolePart(nullptr) 0040 , m_konsolePartCurrentDirectory() 0041 , m_sendCdToTerminalHistory() 0042 , m_kiofuseInterface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus()) 0043 { 0044 m_layout = new QVBoxLayout(this); 0045 m_layout->setContentsMargins(0, 0, 0, 0); 0046 } 0047 0048 TerminalPanel::~TerminalPanel() 0049 { 0050 if (m_konsolePart) { 0051 // Avoid when QObject cleanup, which comes after our destructor, deletes the konsolePart 0052 // and subsequently calls back into our slot when the destructor has already run. 0053 disconnect(m_konsolePart, &KParts::ReadOnlyPart::destroyed, this, &TerminalPanel::terminalExited); 0054 } 0055 } 0056 0057 void TerminalPanel::goHome() 0058 { 0059 sendCdToTerminal(QDir::homePath(), HistoryPolicy::SkipHistory); 0060 } 0061 0062 bool TerminalPanel::currentWorkingDirectoryIsChildOf(const QString &path) const 0063 { 0064 if (m_terminal) { 0065 return m_terminal->currentWorkingDirectory().startsWith(path); 0066 } 0067 return false; 0068 } 0069 0070 void TerminalPanel::terminalExited() 0071 { 0072 m_terminal = nullptr; 0073 m_konsolePart = nullptr; 0074 Q_EMIT hideTerminalPanel(); 0075 } 0076 0077 bool TerminalPanel::isHiddenInVisibleWindow() const 0078 { 0079 return parentWidget() && parentWidget()->isHidden(); 0080 } 0081 0082 void TerminalPanel::dockVisibilityChanged() 0083 { 0084 // Only react when the DockWidget itself (not some parent) is hidden. This way we don't 0085 // respond when e.g. Dolphin is minimized. 0086 if (isHiddenInVisibleWindow()) { 0087 if (m_konsolePartMissingMessage) { 0088 m_konsolePartMissingMessage->hide(); 0089 } else if (m_terminal && !hasProgramRunning()) { 0090 // Make sure that the following "cd /" command will not affect the view. 0091 disconnect(m_konsolePart, SIGNAL(currentDirectoryChanged(QString)), this, SLOT(slotKonsolePartCurrentDirectoryChanged(QString))); 0092 0093 // Make sure this terminal does not prevent unmounting any removable drives 0094 changeDir(QUrl::fromLocalFile(QStringLiteral("/"))); 0095 0096 // Because we have disconnected from the part's currentDirectoryChanged() 0097 // signal, we have to update m_konsolePartCurrentDirectory manually. If this 0098 // was not done, showing the panel again might not set the part's working 0099 // directory correctly. 0100 m_konsolePartCurrentDirectory = '/'; 0101 } 0102 } 0103 } 0104 0105 QString TerminalPanel::runningProgramName() const 0106 { 0107 return m_terminal ? m_terminal->foregroundProcessName() : QString(); 0108 } 0109 0110 KActionCollection *TerminalPanel::actionCollection() 0111 { 0112 // m_terminal is the only reference reset to nullptr in case the terminal is 0113 // closed again 0114 if (m_terminal && m_konsolePart && m_terminalWidget) { 0115 const auto guiClients = m_konsolePart->childClients(); 0116 for (auto *client : guiClients) { 0117 if (client->actionCollection()->associatedWidgets().contains(m_terminalWidget)) { 0118 return client->actionCollection(); 0119 } 0120 } 0121 } 0122 return nullptr; 0123 } 0124 0125 bool TerminalPanel::hasProgramRunning() const 0126 { 0127 return m_terminal && (m_terminal->foregroundProcessId() != -1); 0128 } 0129 0130 bool TerminalPanel::urlChanged() 0131 { 0132 if (!url().isValid()) { 0133 return false; 0134 } 0135 0136 const bool sendInput = m_terminal && !hasProgramRunning() && isVisible(); 0137 if (sendInput) { 0138 changeDir(url()); 0139 } 0140 0141 return true; 0142 } 0143 0144 void TerminalPanel::showEvent(QShowEvent *event) 0145 { 0146 if (event->spontaneous()) { 0147 Panel::showEvent(event); 0148 return; 0149 } 0150 0151 if (!m_terminal) { 0152 m_clearTerminal = true; 0153 KPluginFactory *factory = KPluginFactory::loadFactory(KPluginMetaData(QStringLiteral("kf6/parts/konsolepart"))).plugin; 0154 m_konsolePart = factory ? (factory->create<KParts::ReadOnlyPart>(this)) : nullptr; 0155 if (m_konsolePart) { 0156 connect(m_konsolePart, &KParts::ReadOnlyPart::destroyed, this, &TerminalPanel::terminalExited); 0157 m_terminalWidget = m_konsolePart->widget(); 0158 setFocusProxy(m_terminalWidget); 0159 m_layout->addWidget(m_terminalWidget); 0160 if (m_konsolePartMissingMessage) { 0161 m_layout->removeWidget(m_konsolePartMissingMessage); 0162 } 0163 m_terminal = qobject_cast<TerminalInterface *>(m_konsolePart); 0164 0165 // needed to collect the correct KonsolePart actionCollection 0166 // namely the one of the single inner terminal and not the outer KonsolePart 0167 if (!m_konsolePart->factory() && m_terminalWidget) { 0168 if (!m_konsolePart->clientBuilder()) { 0169 m_konsolePart->setClientBuilder(new KXMLGUIBuilder(m_terminalWidget)); 0170 } 0171 0172 auto factory = new KXMLGUIFactory(m_konsolePart->clientBuilder(), this); 0173 factory->addClient(m_konsolePart); 0174 0175 // Prevents the KXMLGui warning about removing the client 0176 connect(m_terminalWidget, &QObject::destroyed, this, [factory, this] { 0177 factory->removeClient(m_konsolePart); 0178 }); 0179 } 0180 0181 } else { 0182 if (!m_konsolePartMissingMessage) { 0183 const auto konsoleInstallUrl = QUrl("appstream://org.kde.konsole.desktop"); 0184 const auto konsoleNotInstalledText = i18n( 0185 "Terminal cannot be shown because Konsole is not installed. " 0186 "Please install it and then reopen the panel."); 0187 m_konsolePartMissingMessage = new KMessageWidget(konsoleNotInstalledText, this); 0188 m_konsolePartMissingMessage->setPosition(KMessageWidget::Footer); 0189 m_konsolePartMissingMessage->setCloseButtonVisible(false); 0190 m_konsolePartMissingMessage->hide(); 0191 if (KIO::DesktopExecParser::hasSchemeHandler(konsoleInstallUrl)) { 0192 auto installKonsoleAction = new QAction(i18n("Install Konsole"), this); 0193 connect(installKonsoleAction, &QAction::triggered, [konsoleInstallUrl]() { 0194 QDesktopServices::openUrl(konsoleInstallUrl); 0195 }); 0196 m_konsolePartMissingMessage->addAction(installKonsoleAction); 0197 } 0198 m_layout->addWidget(m_konsolePartMissingMessage); 0199 m_layout->setSizeConstraint(QLayout::SetMaximumSize); 0200 } 0201 0202 m_konsolePartMissingMessage->animatedShow(); 0203 } 0204 } 0205 if (m_terminal) { 0206 m_terminal->showShellInDir(url().toLocalFile()); 0207 if (!hasProgramRunning()) { 0208 changeDir(url()); 0209 } 0210 m_terminalWidget->setFocus(); 0211 connect(m_konsolePart, SIGNAL(currentDirectoryChanged(QString)), this, SLOT(slotKonsolePartCurrentDirectoryChanged(QString))); 0212 } 0213 0214 Panel::showEvent(event); 0215 } 0216 0217 void TerminalPanel::changeDir(const QUrl &url) 0218 { 0219 delete m_mostLocalUrlJob; 0220 m_mostLocalUrlJob = nullptr; 0221 0222 if (url.isLocalFile()) { 0223 sendCdToTerminal(url.toLocalFile()); 0224 return; 0225 } 0226 0227 // Try stat'ing the url; note that mostLocalUrl only works with ":local" protocols 0228 if (KProtocolInfo::protocolClass(url.scheme()) == QLatin1String(":local")) { 0229 m_mostLocalUrlJob = KIO::mostLocalUrl(url, KIO::HideProgressInfo); 0230 if (m_mostLocalUrlJob->uiDelegate()) { 0231 KJobWidgets::setWindow(m_mostLocalUrlJob, this); 0232 } 0233 connect(m_mostLocalUrlJob, &KIO::StatJob::result, this, &TerminalPanel::slotMostLocalUrlResult); 0234 return; 0235 } 0236 0237 // Last chance, try KIOFuse 0238 sendCdToTerminalKIOFuse(url); 0239 } 0240 0241 void TerminalPanel::sendCdToTerminal(const QString &dir, HistoryPolicy addToHistory) 0242 { 0243 if (dir == m_konsolePartCurrentDirectory // We are already there 0244 && m_sendCdToTerminalHistory.isEmpty() // …and that is not because the terminal couldn't keep up 0245 ) { 0246 m_clearTerminal = false; 0247 return; 0248 } 0249 0250 // Send prior Ctrl-E, Ctrl-U to ensure the line is empty. This is 0251 // mandatory, otherwise sending a 'cd x\n' to a prompt with 'rm -rf *' 0252 // would result in data loss. 0253 m_terminal->sendInput(QStringLiteral("\x05\x15")); 0254 0255 // We want to ignore the currentDirectoryChanged(QString) signal, which we will receive after 0256 // the directory change, because this directory change is not caused by a "cd" command that the 0257 // user entered in the panel. Therefore, we have to remember 'dir'. Note that it could also be 0258 // a symbolic link -> remember the 'canonical' path. 0259 if (addToHistory == HistoryPolicy::AddToHistory) 0260 m_sendCdToTerminalHistory.enqueue(QDir(dir).canonicalPath()); 0261 0262 m_terminal->sendInput(" cd " + KShell::quoteArg(dir) + '\r'); 0263 0264 if (m_clearTerminal) { 0265 m_terminal->sendInput(QStringLiteral(" clear\r")); 0266 m_clearTerminal = false; 0267 } 0268 } 0269 0270 void TerminalPanel::sendCdToTerminalKIOFuse(const QUrl &url) 0271 { 0272 // URL isn't local, only hope for the terminal to be in sync with the 0273 // DolphinView is to mount the remote URL in KIOFuse and point to it. 0274 // If we can't do that for any reason, silently fail. 0275 auto reply = m_kiofuseInterface.mountUrl(url.toString()); 0276 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); 0277 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *watcher) { 0278 watcher->deleteLater(); 0279 if (!reply.isError()) { 0280 // Successfully mounted, point to the KIOFuse equivalent path. 0281 sendCdToTerminal(reply.value()); 0282 } 0283 }); 0284 } 0285 0286 void TerminalPanel::slotMostLocalUrlResult(KJob *job) 0287 { 0288 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job); 0289 const QUrl url = statJob->mostLocalUrl(); 0290 if (url.isLocalFile()) { 0291 sendCdToTerminal(url.toLocalFile()); 0292 } else { 0293 sendCdToTerminalKIOFuse(url); 0294 } 0295 0296 m_mostLocalUrlJob = nullptr; 0297 } 0298 0299 void TerminalPanel::slotKonsolePartCurrentDirectoryChanged(const QString &dir) 0300 { 0301 m_konsolePartCurrentDirectory = QDir(dir).canonicalPath(); 0302 0303 // Only emit a changeUrl signal if the directory change was caused by the user inside the 0304 // terminal, and not by sendCdToTerminal(QString). 0305 while (!m_sendCdToTerminalHistory.empty()) { 0306 if (m_konsolePartCurrentDirectory == m_sendCdToTerminalHistory.dequeue()) { 0307 return; 0308 } 0309 } 0310 0311 // User may potentially be browsing inside a KIOFuse mount. 0312 // If so lets try and change the DolphinView to point to the remote URL equivalent. 0313 // instead of into the KIOFuse mount itself (which can cause performance issues!) 0314 const QUrl url(QUrl::fromLocalFile(dir)); 0315 0316 KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByPath(m_konsolePartCurrentDirectory); 0317 if (mountPoint && mountPoint->mountType() != QStringLiteral("fuse.kio-fuse")) { 0318 // Not in KIOFUse mount, so just switch to the corresponding URL. 0319 Q_EMIT changeUrl(url); 0320 return; 0321 } 0322 0323 auto reply = m_kiofuseInterface.remoteUrl(m_konsolePartCurrentDirectory); 0324 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); 0325 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [=](QDBusPendingCallWatcher *watcher) { 0326 watcher->deleteLater(); 0327 if (reply.isError()) { 0328 // KIOFuse errored out... just show the normal URL 0329 Q_EMIT changeUrl(url); 0330 } else { 0331 // Our location happens to be in a KIOFuse mount and is mounted. 0332 // Let's change the DolphinView to point to the remote URL equivalent. 0333 Q_EMIT changeUrl(QUrl::fromUserInput(reply.value())); 0334 } 0335 }); 0336 } 0337 0338 bool TerminalPanel::terminalHasFocus() const 0339 { 0340 if (m_terminalWidget) { 0341 return m_terminalWidget->hasFocus(); 0342 } 0343 0344 return hasFocus(); 0345 } 0346 0347 #include "moc_terminalpanel.cpp"