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"