File indexing completed on 2024-05-26 04:57:39

0001 /**
0002  * \file textcliformatter.cpp
0003  * CLI formatter for standard text input and output.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 28 Jul 2019
0008  *
0009  * Copyright (C) 2019-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "textcliformatter.h"
0028 #include <QDir>
0029 #include <QStringBuilder>
0030 #include "clierror.h"
0031 #include "abstractcli.h"
0032 #include "frame.h"
0033 
0034 /** @cond */
0035 namespace {
0036 
0037 /**
0038  * Split string into command line arguments supporting quotes and escape
0039  * characters.
0040  * @param str command line string
0041  * @return list of arguments.
0042  */
0043 QStringList splitArgs(const QString& str)
0044 {
0045   QStringList params;
0046 
0047   for (int pos = 0; ; ) {
0048     QChar c;
0049     do {
0050       if (pos >= str.size())
0051         return params;
0052       c = str.at(pos++);
0053     } while (c.isSpace());
0054     QString param = QLatin1String("");
0055     if (c == QLatin1Char('~')) {
0056       if (pos >= str.size() || str.at(pos).isSpace()) {
0057         params.append(QDir::homePath());
0058         continue;
0059       }
0060       if (str.at(pos) == QLatin1Char('/')) {
0061         param = QDir::homePath();
0062         c = QLatin1Char('/');
0063         ++pos;
0064       }
0065     }
0066     do {
0067       if (c == QLatin1Char('"') || c == QLatin1Char('\'')) {
0068         const QChar quote = c;
0069         for (;;) {
0070           if (pos >= str.size())
0071             return QStringList();
0072           c = str.at(pos++);
0073           if (c == quote)
0074             break;
0075           if (c == QLatin1Char('\\')) {
0076             if (pos >= str.size())
0077               return QStringList();
0078             c = str.at(pos++);
0079             if (c != quote && c != QLatin1Char('\\'))
0080               param += QLatin1Char('\\');
0081           }
0082           param += c;
0083         }
0084       } else {
0085         if (c == QLatin1Char('\\')) {
0086           if (pos >= str.size())
0087             return QStringList();
0088           c = str.at(pos++);
0089         }
0090         param += c;
0091       }
0092       if (pos >= str.size())
0093         break;
0094       c = str.at(pos++);
0095     } while (!c.isSpace());
0096     params.append(param);
0097   }
0098 }
0099 
0100 /**
0101  * Print list of files.
0102  * @param io CLI I/O
0103  * @param lst file properties
0104  * @param indent number of spaces to indent
0105  */
0106 void printFiles(AbstractCliIO* io, const QVariantList& lst, int indent)
0107 {
0108   if (lst.isEmpty())
0109     return;
0110 
0111   for (const QVariant& var : lst) {
0112     const QVariantMap map = var.toMap();
0113     QString propsStr = map.value(QLatin1String("selected")).toBool()
0114         ? QLatin1String(">") : QLatin1String(" ");
0115     propsStr +=
0116         map.value(QLatin1String("changed")).toBool() ? QLatin1String("*") : QLatin1String(" ");
0117     if (map.contains(QLatin1String("tags"))) {
0118       const QVariantList tags = map.value(QLatin1String("tags")).toList();
0119       FOR_ALL_TAGS(tagNr) {
0120         propsStr += tags.contains(1 + tagNr)
0121             ? QLatin1Char('1' + tagNr) : QLatin1Char('-');
0122       }
0123     } else {
0124       propsStr += QString(Frame::Tag_NumValues, QLatin1Char(' '));
0125     }
0126     io->writeLine(propsStr + QString(indent, QLatin1Char(' ')) +
0127                     map.value(QLatin1String("fileName")).toString());
0128     if (map.contains(QLatin1String("files"))) {
0129       printFiles(io, map.value(QLatin1String("files")).toList(), indent + 2);
0130     }
0131   }
0132 }
0133 
0134 }
0135 /** @endcond */
0136 
0137 
0138 TextCliFormatter::TextCliFormatter(AbstractCliIO* io)
0139   : AbstractCliFormatter(io)
0140 {
0141 }
0142 
0143 TextCliFormatter::~TextCliFormatter()
0144 {
0145 }
0146 
0147 void TextCliFormatter::clear()
0148 {
0149   m_errorMessage.clear();
0150   m_args.clear();
0151 }
0152 
0153 QStringList TextCliFormatter::parseArguments(const QString& line)
0154 {
0155   m_errorMessage.clear();
0156   m_args = splitArgs(line);
0157   return m_args;
0158 }
0159 
0160 QString TextCliFormatter::getErrorMessage() const
0161 {
0162   return m_errorMessage;
0163 }
0164 
0165 bool TextCliFormatter::isIncomplete() const
0166 {
0167   return false;
0168 }
0169 
0170 bool TextCliFormatter::isFormatRecognized() const
0171 {
0172   return !m_args.isEmpty();
0173 }
0174 
0175 void TextCliFormatter::writeError(CliError errorCode)
0176 {
0177   QString errorMsg;
0178   switch (errorCode) {
0179   case CliError::MethodNotFound:
0180 #if QT_VERSION >= 0x050600
0181     errorMsg = tr("Unknown command '%1'. Type 'help' for help.")
0182         .arg(m_args.isEmpty() ? QLatin1String("") : m_args.constFirst());
0183 #else
0184     errorMsg = tr("Unknown command '%1'. Type 'help' for help.")
0185         .arg(m_args.isEmpty() ? QLatin1String("") : m_args.first());
0186 #endif
0187     break;
0188   default:
0189     ;
0190   }
0191   if (!errorMsg.isEmpty()) {
0192     writeError(errorMsg);
0193   }
0194 }
0195 
0196 void TextCliFormatter::writeError(const QString& msg)
0197 {
0198   io()->writeErrorLine(msg);
0199 }
0200 
0201 void TextCliFormatter::writeError(const QString& msg, CliError errorCode)
0202 {
0203   if (errorCode == CliError::Usage) {
0204     io()->writeLine(tr("Usage:"));
0205   }
0206   writeError(msg);
0207 }
0208 
0209 void TextCliFormatter::writeResult(const QString& str)
0210 {
0211   io()->writeLine(str);
0212 }
0213 
0214 /**
0215  * Write result message.
0216  * @param strs result as string list
0217  */
0218 void TextCliFormatter::writeResult(const QStringList& strs)
0219 {
0220   for (const QString& str : strs) {
0221     io()->writeLine(str);
0222   }
0223 }
0224 
0225 void TextCliFormatter::writeResult(bool result)
0226 {
0227   io()->writeLine(QLatin1String(result ? "true" : "false"));
0228 }
0229 
0230 void TextCliFormatter::writeResult(const QVariantMap& map)
0231 {
0232   for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
0233     if (const QString& key = it.key(); key == QLatin1String("tags")) {
0234       QVariantList value = it.value().toList();
0235       QString tagStr;
0236       for (const QVariant& var : value) {
0237         if (!tagStr.isEmpty()) {
0238           tagStr += QLatin1String(", ");
0239         }
0240         tagStr += var.toString();
0241       }
0242       if (tagStr.isEmpty()) {
0243         tagStr = QLatin1String("-");
0244       }
0245       io()->writeLine(tr("Tags") + QLatin1String(": ") + tagStr);
0246     } else if (key == QLatin1String("taggedFile")) {
0247       QVariantMap value = it.value().toMap();
0248       QString m_detailInfo = value.value(QLatin1String("format")).toString();
0249       QString m_filename = value.value(QLatin1String("fileName")).toString();
0250       bool m_fileNameChanged = value.value(QLatin1String("fileNameChanged")).toBool();
0251       if (!m_detailInfo.isEmpty()) {
0252         io()->writeLine(tr("File") + QLatin1String(": ") + m_detailInfo);
0253       }
0254       if (!m_filename.isEmpty()) {
0255         QString line = m_fileNameChanged ? QLatin1String("*") : QLatin1String(" ");
0256         line += QLatin1Char(' ');
0257         line += tr("Name");
0258         line += QLatin1String(": ");
0259         line += m_filename;
0260         io()->writeLine(line);
0261       }
0262       FOR_ALL_TAGS(tagNr) {
0263         QString tagNrStr = Frame::tagNumberToString(tagNr);
0264         if (const QVariantMap tag = value.value(QLatin1String("tag") + tagNrStr).toMap();
0265             !tag.isEmpty()) {
0266           if (const QVariantList frames = tag.value(QLatin1String("frames")).toList();
0267               !frames.isEmpty()) {
0268             int maxLength = 0;
0269             for (const QVariant& var : frames) {
0270               QString name = var.toMap().value(QLatin1String("name")).toString();
0271               maxLength = qMax(name.size(), maxLength);
0272             }
0273             QString tagStr = tag.value(QLatin1String("format")).toString();
0274             if (!tagStr.isEmpty()) {
0275               tagStr.prepend(QLatin1Char(' '));
0276             }
0277             tagStr.prepend(QLatin1Char(':'));
0278             tagStr.prepend(tr("Tag %1").arg(tagNrStr));
0279             io()->writeLine(tagStr);
0280             for (const QVariant& var : frames) {
0281               QString name = var.toMap().value(QLatin1String("name")).toString();
0282               QString frameValue = var.toMap().value(QLatin1String("value")).toString();
0283               bool changed =  var.toMap().value(QLatin1String("changed")).toBool();
0284               QString line = changed ? QLatin1String("*") : QLatin1String(" ");
0285               line += QLatin1Char(' ');
0286               line += name;
0287               line += QString(maxLength - name.size() + 2,
0288                               QLatin1Char(' '));
0289               line += frameValue;
0290               io()->writeLine(line);
0291             }
0292           }
0293         }
0294       }
0295     } else if (key == QLatin1String("files")) {
0296       printFiles(io(), it.value().toList(), 1);
0297     } else if (key == QLatin1String("timeout")) {
0298       QString value = it.value().toString();
0299       io()->writeLine(tr("Timeout") % QLatin1String(": ") % value);
0300     } else if (key == QLatin1String("event")) {
0301       QVariantMap value = it.value().toMap();
0302       QString type = value.value(QLatin1String("type")).toString();
0303       QString eventText;
0304       if (type == QLatin1String("readingDirectory")) {
0305         eventText = tr("Reading Folder");
0306       } else if (type == QLatin1String("started")) {
0307         eventText = tr("Started");
0308       } else if (type == QLatin1String("source")) {
0309         eventText = tr("Source");
0310       } else if (type == QLatin1String("querying")) {
0311         eventText = tr("Querying");
0312       } else if (type == QLatin1String("fetching")) {
0313         eventText = tr("Fetching");
0314       } else if (type == QLatin1String("dataReceived")) {
0315         eventText = tr("Data received");
0316       } else if (type == QLatin1String("cover")) {
0317         eventText = tr("Cover");
0318       } else if (type == QLatin1String("finished")) {
0319         eventText = tr("Finished");
0320       } else if (type == QLatin1String("aborted")) {
0321         eventText = tr("Aborted");
0322       } else if (type == QLatin1String("error")) {
0323         eventText = tr("Error");
0324       } else if (type == QLatin1String("parseError")) {
0325         eventText = QLatin1String("parse error");
0326       } else {
0327         eventText = type;
0328       }
0329       QVariant data = value.value(QLatin1String("data"));
0330 #if QT_VERSION >= 0x060000
0331       if (data.typeId() == QMetaType::QString) {
0332 #else
0333       if (data.type() == QVariant::String) {
0334 #endif
0335         if (QString text = data.toString(); !text.isEmpty()) {
0336           if (type == QLatin1String("filterEntered")) {
0337             eventText = QLatin1String("  ") + text;
0338           } else if (type == QLatin1String("filterPassed")) {
0339             eventText = QLatin1String("+ ") + text;
0340           } else if (type == QLatin1String("filteredOut")) {
0341             eventText = QLatin1String("- ") + text;
0342           } else {
0343             eventText += QLatin1String(": ");
0344             eventText += text;
0345           }
0346         }
0347 #if QT_VERSION >= 0x060000
0348       } else if (data.typeId() == QMetaType::QVariantMap) {
0349 #else
0350       } else if (data.type() == QVariant::Map) {
0351 #endif
0352         // Event maps with source and destination are used with
0353         // rename directory events.
0354         QVariantMap dataMap = data.toMap();
0355         if (dataMap.contains(QLatin1String("source"))) {
0356           eventText += QLatin1String("  ");
0357           eventText += dataMap.value(QLatin1String("source")).toString();
0358         }
0359         if (dataMap.contains(QLatin1String("destination"))) {
0360           eventText += QLatin1String("\n  ");
0361           eventText += dataMap.value(QLatin1String("destination")).toString();
0362         }
0363       }
0364       io()->writeLine(eventText);
0365     }
0366   }
0367 }
0368 
0369 void TextCliFormatter::finishWriting()
0370 {
0371 }