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 }