File indexing completed on 2024-05-19 05:54:12

0001 // This file was part of the KDE libraries
0002 // SPDX-FileCopyrightText: 2022 Tao Guo <guotao945@gmail.com>
0003 // SPDX-License-Identifier: GPL-2.0-or-later
0004 
0005 #include "quickcommandswidget.h"
0006 #include "filtermodel.h"
0007 #include "konsoledebug.h"
0008 #include "terminalDisplay/TerminalDisplay.h"
0009 
0010 #include <QMenu>
0011 
0012 #include "ui_qcwidget.h"
0013 #include <KLocalizedString>
0014 #include <KMessageBox>
0015 #include <kwidgetsaddons_version.h>
0016 
0017 #include <QSettings>
0018 #include <QStandardPaths>
0019 #include <QTemporaryFile>
0020 #include <QTimer>
0021 
0022 #include <QShowEvent>
0023 
0024 #include <kmessagebox.h>
0025 #include <kstandardguiitem.h>
0026 
0027 struct QuickCommandsWidget::Private {
0028     QuickCommandsModel *model = nullptr;
0029     FilterModel *filterModel = nullptr;
0030     Konsole::SessionController *controller = nullptr;
0031     bool hasShellCheck = false;
0032     bool isSetup = false;
0033     QTimer shellCheckTimer;
0034 };
0035 
0036 QuickCommandsWidget::QuickCommandsWidget(QWidget *parent)
0037     : QWidget(parent)
0038     , ui(std::make_unique<Ui::QuickCommandsWidget>())
0039     , priv(std::make_unique<Private>())
0040 {
0041     ui->setupUi(this);
0042 
0043     priv->hasShellCheck = !QStandardPaths::findExecutable(QStringLiteral("shellcheck")).isEmpty();
0044     if (!priv->hasShellCheck) {
0045         ui->warning->setPlainText(QStringLiteral("Missing executable 'shellcheck', please install"));
0046     }
0047     priv->shellCheckTimer.setSingleShot(true);
0048 
0049     priv->filterModel = new FilterModel(this);
0050     connect(ui->btnAdd, &QPushButton::clicked, this, &QuickCommandsWidget::addMode);
0051     connect(ui->btnSave, &QPushButton::clicked, this, &QuickCommandsWidget::saveCommand);
0052     connect(ui->btnUpdate, &QPushButton::clicked, this, &QuickCommandsWidget::updateCommand);
0053     connect(ui->btnCancel, &QPushButton::clicked, this, &QuickCommandsWidget::viewMode);
0054     connect(ui->btnRun, &QPushButton::clicked, this, &QuickCommandsWidget::runCommand);
0055 
0056     connect(ui->invertFilter, &QPushButton::clicked, priv->filterModel, &FilterModel::setInvertFilter);
0057 
0058     connect(ui->filterLine, &QLineEdit::textChanged, this, [this] {
0059         priv->filterModel->setFilterRegularExpression(ui->filterLine->text());
0060         priv->filterModel->invalidate();
0061     });
0062 
0063     ui->commandsTreeView->setModel(priv->filterModel);
0064     ui->commandsTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
0065     connect(ui->commandsTreeView, &QTreeView::doubleClicked, this, &QuickCommandsWidget::invokeCommand);
0066     connect(ui->commandsTreeView, &QTreeView::clicked, this, &QuickCommandsWidget::indexSelected);
0067 
0068     connect(ui->commandsTreeView, &QTreeView::customContextMenuRequested, this, &QuickCommandsWidget::createMenu);
0069 
0070     connect(&priv->shellCheckTimer, &QTimer::timeout, this, &QuickCommandsWidget::runShellCheck);
0071     connect(ui->command, &QPlainTextEdit::textChanged, this, [this] {
0072         priv->shellCheckTimer.start(250);
0073     });
0074 
0075     viewMode();
0076 
0077     QSettings settings;
0078     settings.beginGroup(QStringLiteral("plugins"));
0079     settings.beginGroup(QStringLiteral("quickcommands"));
0080 
0081     const QKeySequence def(Qt::CTRL | Qt::ALT | Qt::Key_G);
0082     const QString defText = def.toString();
0083     const QString entry = settings.value(QStringLiteral("shortcut"), defText).toString();
0084     const QKeySequence shortcutEntry(entry);
0085 
0086     connect(ui->keySequenceEdit, &QKeySequenceEdit::keySequenceChanged, this, [this] {
0087         auto shortcut = ui->keySequenceEdit->keySequence();
0088         Q_EMIT quickAccessShortcutChanged(shortcut);
0089     });
0090     ui->keySequenceEdit->setKeySequence(shortcutEntry);
0091 }
0092 
0093 QuickCommandsWidget::~QuickCommandsWidget() = default;
0094 
0095 void QuickCommandsWidget::prepareEdit()
0096 {
0097     QString groupName = ui->group->currentText();
0098 
0099     ui->group->clear();
0100     ui->group->addItems(priv->model->groups());
0101     ui->group->setCurrentText(groupName);
0102     ui->commandsTreeView->setDisabled(true);
0103 
0104     ui->commandsWidget->show();
0105 }
0106 void QuickCommandsWidget::viewMode()
0107 {
0108     ui->commandsTreeView->setDisabled(false);
0109     ui->commandsWidget->hide();
0110     ui->btnAdd->show();
0111     ui->btnSave->hide();
0112     ui->btnUpdate->hide();
0113     ui->btnCancel->hide();
0114 }
0115 
0116 void QuickCommandsWidget::addMode()
0117 {
0118     ui->btnAdd->hide();
0119     ui->btnSave->show();
0120     ui->btnUpdate->hide();
0121     ui->btnCancel->show();
0122     prepareEdit();
0123 }
0124 
0125 void QuickCommandsWidget::indexSelected(const QModelIndex &idx)
0126 {
0127     Q_UNUSED(idx)
0128 
0129     const auto sourceIdx = priv->filterModel->mapToSource(ui->commandsTreeView->currentIndex());
0130     if (priv->model->rowCount(sourceIdx) != 0) {
0131         ui->name->setText({});
0132         ui->tooltip->setText({});
0133         ui->command->setPlainText({});
0134         ui->group->setCurrentText({});
0135         return;
0136     }
0137 
0138     const auto item = priv->model->itemFromIndex(sourceIdx);
0139 
0140     if (item != nullptr && item->parent() != nullptr) {
0141         const auto data = item->data(QuickCommandsModel::QuickCommandRole).value<QuickCommandData>();
0142         ui->name->setText(data.name);
0143         ui->tooltip->setText(data.tooltip);
0144         ui->command->setPlainText(data.command);
0145         ui->group->setCurrentText(item->parent()->text());
0146 
0147         runShellCheck();
0148     }
0149 }
0150 
0151 void QuickCommandsWidget::editMode()
0152 {
0153     ui->btnAdd->hide();
0154     ui->btnSave->hide();
0155     ui->btnUpdate->show();
0156     ui->btnCancel->show();
0157     prepareEdit();
0158 }
0159 
0160 void QuickCommandsWidget::saveCommand()
0161 {
0162     if (!valid())
0163         return;
0164     if (priv->model->addChildItem(data(), ui->group->currentText()))
0165         viewMode();
0166     else
0167         KMessageBox::error(this, i18n("A duplicate item exists"));
0168 }
0169 
0170 void QuickCommandsWidget::updateCommand()
0171 {
0172     const auto sourceIdx = priv->filterModel->mapToSource(ui->commandsTreeView->currentIndex());
0173     if (!valid())
0174         return;
0175     if (priv->model->editChildItem(data(), sourceIdx, ui->group->currentText()))
0176         viewMode();
0177     else
0178         KMessageBox::error(this, i18n("A duplicate item exists"));
0179 }
0180 
0181 void QuickCommandsWidget::invokeCommand(const QModelIndex &idx)
0182 {
0183     if (!ui->warning->toPlainText().isEmpty()) {
0184         QMessageBox::warning(this, QStringLiteral("Shell Errors"), i18n("Please fix all the warnings before trying to run this script"));
0185         return;
0186     }
0187 
0188     if (!priv->controller) {
0189         return;
0190     }
0191     const auto sourceIdx = priv->filterModel->mapToSource(idx);
0192     if (sourceIdx.parent() == priv->model->invisibleRootItem()->index()) {
0193         return;
0194     }
0195     const auto item = priv->model->itemFromIndex(sourceIdx);
0196     const auto data = item->data(QuickCommandsModel::QuickCommandRole).value<QuickCommandData>();
0197     priv->controller->session()->sendTextToTerminal(data.command, QLatin1Char('\r'));
0198 
0199     if (priv->controller->session()->views().count()) {
0200         priv->controller->session()->views().at(0)->setFocus();
0201     }
0202 }
0203 
0204 void QuickCommandsWidget::runCommand()
0205 {
0206     if (!priv->hasShellCheck) {
0207         // check again
0208         priv->hasShellCheck = !QStandardPaths::findExecutable(QStringLiteral("shellcheck")).isEmpty();
0209         if (priv->hasShellCheck) {
0210             ui->warning->clear();
0211         }
0212     }
0213 
0214     if (!ui->warning->toPlainText().isEmpty()) {
0215         auto choice = KMessageBox::questionTwoActions(this,
0216                                                       i18n("There are some errors on the script, do you really want to run it?"),
0217                                                       i18n("Shell Errors"),
0218                                                       KGuiItem(i18nc("@action:button", "Run"), QStringLiteral("system-run")),
0219                                                       KStandardGuiItem::cancel(),
0220                                                       QStringLiteral("quick-commands-question"));
0221         if (choice == KMessageBox::SecondaryAction) {
0222             return;
0223         }
0224     }
0225 
0226     const QString command = ui->command->toPlainText();
0227     priv->controller->session()->sendTextToTerminal(command, QLatin1Char('\r'));
0228     if (priv->controller->session()->views().count()) {
0229         priv->controller->session()->views().at(0)->setFocus();
0230     }
0231 }
0232 
0233 void QuickCommandsWidget::triggerRename()
0234 {
0235     ui->commandsTreeView->edit(ui->commandsTreeView->currentIndex());
0236 }
0237 
0238 void QuickCommandsWidget::triggerDelete()
0239 {
0240     const auto idx = ui->commandsTreeView->currentIndex();
0241     const QString text = idx.data(Qt::DisplayRole).toString();
0242     const QString dialogMessage = ui->commandsTreeView->model()->rowCount(idx)
0243         ? i18n("You are about to delete the group %1,\n with multiple configurations, are you sure?", text)
0244         : i18n("You are about to delete %1, are you sure?", text);
0245 
0246     int result =
0247         KMessageBox::warningTwoActions(this, dialogMessage, i18n("Delete Quick Commands Configurations"), KStandardGuiItem::del(), KStandardGuiItem::cancel());
0248     if (result != KMessageBox::PrimaryAction)
0249         return;
0250 
0251     const auto sourceIdx = priv->filterModel->mapToSource(idx);
0252     priv->model->removeRow(sourceIdx.row(), sourceIdx.parent());
0253 }
0254 
0255 QuickCommandData QuickCommandsWidget::data() const
0256 {
0257     QuickCommandData data;
0258     data.name = ui->name->text().trimmed();
0259     data.tooltip = ui->tooltip->text();
0260     data.command = ui->command->toPlainText();
0261     return data;
0262 }
0263 
0264 void QuickCommandsWidget::setModel(QuickCommandsModel *model)
0265 {
0266     priv->model = model;
0267     priv->filterModel->setSourceModel(model);
0268 }
0269 void QuickCommandsWidget::setCurrentController(Konsole::SessionController *controller)
0270 {
0271     priv->controller = controller;
0272 }
0273 
0274 bool QuickCommandsWidget::valid()
0275 {
0276     if (ui->name->text().isEmpty() || ui->name->text().trimmed().isEmpty()) {
0277         KMessageBox::error(this, i18n("Title can not be empty or blank"));
0278         return false;
0279     }
0280     if (ui->command->toPlainText().isEmpty()) {
0281         KMessageBox::error(this, i18n("Command can not be empty"));
0282         return false;
0283     }
0284     return true;
0285 }
0286 
0287 void QuickCommandsWidget::createMenu(const QPoint &pos)
0288 {
0289     QModelIndex idx = ui->commandsTreeView->indexAt(pos);
0290     if (!idx.isValid())
0291         return;
0292     auto sourceIdx = priv->filterModel->mapToSource(idx);
0293     const bool isParent = sourceIdx.parent() == priv->model->invisibleRootItem()->index();
0294     QMenu *menu = new QMenu(this);
0295 
0296     if (!isParent) {
0297         auto actionEdit = new QAction(i18n("Edit"), ui->commandsTreeView);
0298         menu->addAction(actionEdit);
0299         connect(actionEdit, &QAction::triggered, this, &QuickCommandsWidget::editMode);
0300     } else {
0301         auto actionRename = new QAction(i18n("Rename"), ui->commandsTreeView);
0302         menu->addAction(actionRename);
0303         connect(actionRename, &QAction::triggered, this, &QuickCommandsWidget::triggerRename);
0304     }
0305     auto actionDelete = new QAction(i18n("Delete"), ui->commandsTreeView);
0306     menu->addAction(actionDelete);
0307     connect(actionDelete, &QAction::triggered, this, &QuickCommandsWidget::triggerDelete);
0308     menu->popup(ui->commandsTreeView->viewport()->mapToGlobal(pos));
0309 }
0310 
0311 void QuickCommandsWidget::runShellCheck()
0312 {
0313     if (!priv->hasShellCheck) {
0314         return;
0315     }
0316 
0317     QTemporaryFile file;
0318     file.open();
0319 
0320     QTextStream ts(&file);
0321     ts << "#!/bin/bash\n";
0322     ts << ui->command->toPlainText();
0323     file.close();
0324 
0325     QString fName = file.fileName();
0326     QProcess process;
0327     process.start(QStringLiteral("shellcheck"), {fName});
0328     process.waitForFinished();
0329 
0330     const QString errorString = QString::fromLocal8Bit(process.readAllStandardOutput());
0331     ui->warning->setPlainText(errorString);
0332 
0333     if (errorString.isEmpty()) {
0334         ui->tabWidget->setTabText(1, i18n("Warnings"));
0335     } else {
0336         ui->tabWidget->setTabText(1, i18n("Warnings (*)"));
0337     }
0338 }
0339 
0340 void QuickCommandsWidget::showEvent(QShowEvent *)
0341 {
0342     if (!priv->isSetup) {
0343         ui->commandsTreeView->expandAll();
0344         priv->isSetup = true;
0345     }
0346 }
0347 
0348 #include "moc_quickcommandswidget.cpp"