File indexing completed on 2024-04-28 11:20:39

0001 /*
0002     SPDX-FileCopyrightText: 2009 Milian Wolff <mail@milianw.de>
0003     SPDX-FileCopyrightText: 2011 Matteo Agostinelli <agostinelli@gmail.com>
0004     SPDX-FileCopyrightText: 2022 Alexander Semke (alexander.semke@web.de)
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "settings.h"
0010 
0011 #include "qalculatesession.h"
0012 #include "qalculatecompletionobject.h"
0013 #include "qalculatehighlighter.h"
0014 #include "defaultvariablemodel.h"
0015 
0016 #include <QProcess>
0017 #include <QRegularExpression>
0018 
0019 #include <libqalculate/Calculator.h>
0020 #include <libqalculate/ExpressionItem.h>
0021 #include <libqalculate/Unit.h>
0022 #include <libqalculate/Prefix.h>
0023 #include <libqalculate/Function.h>
0024 
0025 #include "qalculatesyntaxhelpobject.h"
0026 
0027 QalculateSession::QalculateSession( Cantor::Backend* backend)
0028     : Session(backend),
0029       m_variableModel(new Cantor::DefaultVariableModel(this)),
0030       m_process(nullptr),
0031       m_currentExpression(nullptr),
0032       m_isSaveCommand(false)
0033 {
0034     /*
0035         qalc does all of this by default but we still need the CALCULATOR instance for plotting
0036         graphs
0037     */
0038 
0039     if ( !CALCULATOR ) {
0040              new Calculator();
0041              CALCULATOR->loadGlobalDefinitions();
0042              CALCULATOR->loadLocalDefinitions();
0043              CALCULATOR->loadExchangeRates();
0044     }
0045 }
0046 
0047 QalculateSession::~QalculateSession()
0048 {
0049     CALCULATOR->abort();
0050     if(m_process)
0051     {
0052         m_process->kill();
0053         m_process->deleteLater();
0054         m_process = nullptr;
0055     }
0056 }
0057 
0058 void QalculateSession::login()
0059 {
0060     if (m_process)
0061         return;
0062 
0063     emit loginStarted();
0064     qDebug() << "login started";
0065 
0066     /* we will , most probably, use autoscripts for setting the mode , evaluate options, print options etc */
0067 
0068     // if(!QalculateSettings::autorunScripts().isEmpty()){
0069     //     QString autorunScripts = QalculateSettings::self()->autorunScripts().join(QLatin1String("\n"));
0070     //
0071     //     evaluateExpression(autorunScripts, QalculateExpression::DeleteOnFinish);
0072     // }
0073 
0074     /*
0075         set up the process here. The program path , arguments(if any),channel modes , and connections should all be set up here.
0076         once the setup is complete, start the process and inform the worksheet that we are ready
0077     */
0078     m_process = new QProcess(this);
0079 
0080     m_process->setProgram(QStandardPaths::findExecutable(QLatin1String("qalc")));
0081     QStringList args{QLatin1String("-s"), QLatin1String("color 0")}; // switch off the colored output
0082     m_process->setArguments(args);
0083     m_process->setProcessChannelMode(QProcess::SeparateChannels);
0084 
0085     connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readOutput()));
0086     connect(m_process, SIGNAL(readyReadStandardError()), this, SLOT(readError()));
0087     connect(m_process, SIGNAL(started()), this, SLOT(processStarted()));
0088 
0089     m_process->start();
0090 
0091     changeStatus(Session::Done);
0092     emit loginDone();
0093 }
0094 
0095 void QalculateSession::readOutput()
0096 {
0097         while(m_process->bytesAvailable()) {
0098                 m_output.append(QString::fromLocal8Bit(m_process->readLine()));
0099                 qDebug() << m_output;
0100         }
0101 
0102         if(m_currentExpression && !m_output.isEmpty() && m_output.trimmed().endsWith(QLatin1String(">"))) {
0103 
0104                 // check if the commandQueue is empty or not . if it's not empty run the "runCommandQueue" function.
0105                 // store the output in finalOutput and clear m_output
0106 
0107                 if(m_currentCommand.trimmed().isEmpty())
0108                     m_output.clear();
0109 
0110                 if(!m_output.toLower().contains(QLatin1String("error")) && m_isSaveCommand) {
0111                         storeVariables(m_currentCommand, m_output);
0112                         m_isSaveCommand = false;
0113                 }
0114 
0115                 m_output = m_output.trimmed();
0116                 m_output.remove(m_currentCommand);
0117                 if (!m_output.isEmpty())
0118                     m_finalOutput.append(m_output);
0119 
0120                 // we tried to perform a save operation but failed(see parseSaveCommand()).In such a case
0121                 // m_output will be empty but m_saveError will contain the error message.
0122                 if(!m_saveError.isEmpty()) {
0123                     m_finalOutput.append(m_saveError);
0124                     m_saveError.clear();
0125                 }
0126 
0127                 m_finalOutput.append(QLatin1String("\n"));
0128                 m_output.clear();
0129 
0130 
0131 
0132                 if (!m_commandQueue.isEmpty())
0133                     runCommandQueue();
0134                 else {
0135                     qDebug () << "parsing output: " << m_finalOutput;
0136                     m_currentExpression->parseOutput(m_finalOutput);
0137                     m_finalOutput.clear();
0138                 }
0139         }
0140 }
0141 
0142 void QalculateSession::storeVariables(QString& currentCmd, QString output)
0143 {
0144 
0145     // internally we pass save(value,variable) command to qlac to save the variables. see parseSaveCommand()
0146     // TODO: if the user if trying to override a default variable(constants etc) or an existing variable, ask the user if he/she wants to override it or not.
0147 
0148     qDebug() << "save command " << currentCmd;
0149 
0150     /**
0151         if we have reached here, we expect our variable model to be updated with new variables.
0152         In case the variable model is not updated, it most probably because we were not able to successfully parse the
0153         current command and output to extract variable and value
0154 
0155         This is probably not the best way to get the variable and value.
0156         But since qalc does not  provide a way to get the list of variables, we will have to stick to parsing
0157     **/
0158     QRegularExpression regex;
0159     // find the value
0160     regex.setPattern(QStringLiteral("^[\\s\\w\\W]+=\\s*([\\w\\W]+)$"));
0161     QRegularExpressionMatch match = regex.match(output);
0162     QString value;
0163     if(match.hasMatch()) {
0164         value = match.captured(1).trimmed();
0165         value.replace(QLatin1String("\n"), QLatin1String(""));
0166         value.remove(QLatin1String(">"));
0167     }
0168 
0169     //find the varaiable.
0170     // ex1: currentCmd = save(10, var_1,category, title): var_1 = variable
0171     // ex2: currentCmd = save(planet(jupter,mass), jupiter_mass, category, title): jupiter_mass = variable
0172 
0173     //  regex.setPattern(QLatin1String("\\s*save\\s*\\(\\s*[\\s\\w]+\\s*,([\\s\\w]+),*[\\w\\W]*\\)\\s*;*$|\\s*save\\s*\\(\\s*[\\s\\w\\W]+\\)\\s*,([\\s\\w]+),*[\\w\\W]*\\)\\s*;*$"));
0174 
0175     regex.setPattern(QStringLiteral("^\\s*save\\s*\\("
0176                                     "(?:.+?(?:\\(.+?,.+?\\))|(?:[^,()]+?)),"
0177                                     "(.+?),"
0178                                     "(?:.+?),"
0179                                     "(?:.+?)\\)\\s*;?$"));
0180     QString var;
0181     match = regex.match(currentCmd);
0182     if (match.hasMatch()) {
0183         var = match.captured(1).trimmed();
0184         var.replace(QLatin1String("\n"), QLatin1String(""));
0185         var.remove(QLatin1String(">"));
0186     }
0187     if(!value.isEmpty() && !var.isEmpty())
0188         variables.insert(var, value);
0189 }
0190 
0191 void QalculateSession::readError()
0192 {
0193     QString error =  QLatin1String(m_process->readAllStandardError());
0194     if(m_currentExpression) {
0195         m_currentExpression->parseError(error);
0196     }
0197 }
0198 
0199 void QalculateSession::processStarted()
0200 {
0201     qDebug() << "process  started " << m_process->program() << m_process->processId();
0202 }
0203 
0204 void QalculateSession::logout()
0205 {
0206     qDebug () << "logging out";
0207     if (!m_process)
0208         return;
0209 
0210     if(status() == Cantor::Session::Running)
0211         interrupt();
0212 
0213     m_process->write("quit\n");
0214     if(!m_process->waitForFinished(1000))
0215         m_process->kill();
0216     m_process->deleteLater();
0217     m_process = nullptr;
0218 
0219     Session::logout();
0220 }
0221 
0222 void QalculateSession::interrupt()
0223 {
0224     qDebug () << "interrupting .... ";
0225     if(m_currentExpression)
0226         m_currentExpression->interrupt();
0227 
0228     m_commandQueue.clear();
0229     m_expressionQueue.clear();
0230     m_output.clear();
0231     m_finalOutput.clear();
0232     m_currentCommand.clear();
0233     m_currentExpression = nullptr;
0234 
0235 }
0236 
0237 void QalculateSession::runExpression()
0238 {
0239     const QString& command = m_currentExpression->command();
0240     foreach(const QString& cmd, command.split(QLatin1Char('\n'))) {
0241         m_commandQueue.enqueue(cmd);
0242     }
0243     runCommandQueue();
0244 }
0245 
0246 
0247 void QalculateSession::runCommandQueue()
0248 {
0249     if (!m_commandQueue.isEmpty()) {
0250         m_currentCommand = m_commandQueue.dequeue();
0251         // parse the current command if it's a save/load/store command
0252         if( m_currentCommand.toLower().trimmed().startsWith(QLatin1String("save")) ||
0253             m_currentCommand.toLower().trimmed().startsWith(QLatin1String("store")) ||
0254             m_currentCommand.trimmed().startsWith(QLatin1String("saveVariables"))) {
0255 
0256                 m_currentCommand = parseSaveCommand(m_currentCommand);
0257             }
0258 
0259         m_currentCommand = m_currentCommand.trimmed();
0260         m_currentCommand += QLatin1String("\n");
0261         m_process->write(m_currentCommand.toLocal8Bit());
0262     }
0263 }
0264 
0265 QString QalculateSession::parseSaveCommand(QString& currentCmd)
0266 {
0267     /*
0268         make sure the command is:
0269         * formatted correctly. e.g if the command is save(value,variable), we have to make sure that there is no space between save and '(', otherwise qalc
0270           waits for user input which is not supported by us as of now
0271         * supported save commands: save(value,variable,[category],[title]), save definitions, save mode, save var, store var,
0272         saveVariables filename
0273     */
0274 
0275     QRegularExpression regex;
0276     regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
0277 
0278     regex.setPattern(QStringLiteral("^\\s*save\\s*definitions\\s*$"));
0279     if(regex.match(currentCmd).hasMatch()) {
0280         // save the variables in  ~/.cantor/backends/qalculate/definitions
0281         currentCmd.clear();
0282         return currentCmd;
0283     }
0284 
0285     regex.setPattern(QStringLiteral("^\\s*save\\s*mode\\s*$"));
0286     if(regex.match(currentCmd).hasMatch()) {
0287         // save the mode in ~/.cantor/backends/qalculate/cantor_qalc.cfg
0288         currentCmd.clear();
0289         return currentCmd;
0290     }
0291 
0292     regex.setPattern(QStringLiteral("^\\s*saveVariables\\s*[\\w\\W]+$"));
0293     if(regex.match(currentCmd).hasMatch()) {
0294         // save the variables in a file
0295         currentCmd.clear();
0296         return currentCmd;
0297     }
0298 
0299     regex.setPattern(QStringLiteral("^\\s*store\\s*([a-zA-Z_]+[\\w]*)|\\s*save\\s*([a-zA-Z_]+[\\w]*)$"));
0300     QRegularExpressionMatch match = regex.match(currentCmd);
0301     if(match.hasMatch()) {
0302         m_isSaveCommand = true;
0303 
0304         QString str = match.captured(1).trimmed();
0305         if (str.isEmpty())
0306             str = match.captured(2).trimmed();
0307 
0308         currentCmd = QStringLiteral("save(%1, %2)").arg(QStringLiteral("ans"), str);
0309 
0310         return currentCmd;
0311     }
0312 
0313     regex.setPattern(QStringLiteral("^\\s*save\\s*(\\([\\w\\W]+\\))\\s*;*$"));
0314     match = regex.match(currentCmd);
0315     if(match.hasMatch()) {
0316         m_isSaveCommand = true;
0317         currentCmd = QStringLiteral("save%1").arg(match.captured(1).trimmed());
0318         return currentCmd;
0319     }
0320 
0321     /*
0322         If we have not returned by this point, it's because:
0323         * we did not parse the save command properly. This might be due to malformed regular expressions.
0324         * or the commnad given by the user is malformed. More likely to happen
0325         In both these cases we will simply return an empty string because we don't want qalc to run malformed queries,
0326         else it would wait for user input and hence Qprocess would never return a complete output and the expression will remain in
0327         'calculating' state
0328     */
0329     m_saveError =  currentCmd + QLatin1String("\nError: Could not save.\n");
0330     return QLatin1String("");
0331 }
0332 
0333 //TODO: unify with the funcion in the base class
0334 void QalculateSession::currentExpressionStatusChanged(Cantor::Expression::Status status)
0335 {
0336     // depending on the status of the expression change the status of the session;
0337     switch (status) {
0338         case Cantor::Expression::Computing:
0339             break;
0340         case Cantor::Expression::Interrupted:
0341             changeStatus(Cantor::Session::Done);
0342             break;
0343         case Cantor::Expression::Queued:
0344             break;
0345         case Cantor::Expression::Done:
0346         case Cantor::Expression::Error:
0347             qDebug() << " ******  STATUS   " << status;
0348             changeStatus(Cantor::Session::Done);
0349             if(m_expressionQueue.size() > 0)
0350                 m_expressionQueue.dequeue();
0351             if(!m_expressionQueue.isEmpty())
0352                 runExpressionQueue();
0353     }
0354 }
0355 
0356 Cantor::Expression* QalculateSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behave, bool internal)
0357 {
0358     qDebug() << " ** evaluating expression: " << cmd;
0359     qDebug() << " size of expression queue: " << m_expressionQueue.size();
0360 
0361     changeStatus(Cantor::Session::Running);
0362 
0363     QalculateExpression* expr = new QalculateExpression(this, internal);
0364     expr->setFinishingBehavior(behave);
0365     expr->setCommand(cmd);
0366 
0367     m_expressionQueue.enqueue(expr);
0368     runExpressionQueue();
0369 
0370     return expr;
0371 }
0372 
0373 void QalculateSession::runExpressionQueue()
0374 {
0375     if(!m_expressionQueue.isEmpty()) {
0376 
0377         if(!m_currentExpression)
0378             m_currentExpression = m_expressionQueue.head();
0379 
0380         else {
0381             /* there was some expression that was being executed by cantor. We run the new expression only
0382                if the current expression's status is 'Done' or 'Error', if not , we simply return
0383             */
0384             Cantor::Expression::Status expr_status = m_currentExpression->status();
0385             if(expr_status != Cantor::Expression::Done &&  expr_status != Cantor::Expression::Error)
0386                 return;
0387         }
0388 
0389         m_currentExpression = m_expressionQueue.head();
0390         connect(m_currentExpression, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SLOT(currentExpressionStatusChanged(Cantor::Expression::Status)));
0391         // start processing the expression
0392         m_currentExpression->evaluate();
0393     }
0394 }
0395 
0396 
0397 Cantor::CompletionObject* QalculateSession::completionFor(const QString& command, int index)
0398 {
0399     return new QalculateCompletionObject(command, index, this);
0400 }
0401 
0402 Cantor::SyntaxHelpObject* QalculateSession::syntaxHelpFor(const QString& cmd)
0403 {
0404     return new QalculateSyntaxHelpObject(cmd, this);
0405 }
0406 
0407 QSyntaxHighlighter* QalculateSession::syntaxHighlighter(QObject* parent)
0408 {
0409     return new QalculateHighlighter(parent);
0410 }
0411 
0412 Cantor::DefaultVariableModel* QalculateSession::variableModel() const
0413 {
0414     return m_variableModel;
0415 }