Browse Source

Task: refine task

Le Tan 3 years ago
parent
commit
70c984353c
63 changed files with 2206 additions and 1995 deletions
  1. 30 13
      src/core/configmgr.cpp
  2. 7 0
      src/core/configmgr.h
  3. 0 10
      src/core/core.pri
  4. 5 1
      src/core/editorconfig.cpp
  5. 5 0
      src/core/notebookmgr.cpp
  6. 2 0
      src/core/notebookmgr.h
  7. 0 102
      src/core/shellexecution.cpp
  8. 0 42
      src/core/shellexecution.h
  9. 0 557
      src/core/task.cpp
  10. 0 185
      src/core/task.h
  11. 0 233
      src/core/taskhelper.cpp
  12. 0 50
      src/core/taskhelper.h
  13. 0 192
      src/core/taskmgr.cpp
  14. 0 79
      src/core/taskmgr.h
  15. 0 334
      src/core/taskvariablemgr.cpp
  16. 0 73
      src/core/taskvariablemgr.h
  17. 3 0
      src/core/vnotex.cpp
  18. 1 1
      src/core/vnotex.h
  19. 0 1
      src/data/core/icons/task_menu.svg
  20. 3 3
      src/data/extra/tasks/git/git.json
  21. 3 1
      src/export/webviewexporter.cpp
  22. 24 7
      src/snippet/snippetmgr.cpp
  23. 7 2
      src/snippet/snippetmgr.h
  24. 2 0
      src/src.pro
  25. 72 0
      src/task/shellexecution.cpp
  26. 34 0
      src/task/shellexecution.h
  27. 588 0
      src/task/task.cpp
  28. 197 0
      src/task/task.h
  29. 13 0
      src/task/task.pri
  30. 111 0
      src/task/taskmgr.cpp
  31. 63 0
      src/task/taskmgr.h
  32. 435 0
      src/task/taskvariablemgr.cpp
  33. 105 0
      src/task/taskvariablemgr.h
  34. 14 8
      src/utils/fileutils.cpp
  35. 1 1
      src/utils/fileutils.h
  36. 0 1
      src/utils/pathutils.cpp
  37. 54 1
      src/utils/utils.cpp
  38. 4 0
      src/utils/utils.h
  39. 1 6
      src/widgets/dialogs/selectdialog.cpp
  40. 9 8
      src/widgets/dockwidgethelper.cpp
  41. 2 2
      src/widgets/dockwidgethelper.h
  42. 0 6
      src/widgets/framelessmainwindow/framelessmainwindowlinux.cpp
  43. 1 1
      src/widgets/framelessmainwindow/framelessmainwindowlinux.h
  44. 0 6
      src/widgets/framelessmainwindow/framelessmainwindowwin.cpp
  45. 1 1
      src/widgets/framelessmainwindow/framelessmainwindowwin.h
  46. 8 15
      src/widgets/mainwindow.cpp
  47. 3 2
      src/widgets/mainwindow.h
  48. 1 1
      src/widgets/notebooknodeexplorer.cpp
  49. 62 0
      src/widgets/terminalviewer.cpp
  50. 33 0
      src/widgets/terminalviewer.h
  51. 68 25
      src/widgets/toolbarhelper.cpp
  52. 2 0
      src/widgets/toolbarhelper.h
  53. 2 2
      src/widgets/viewwindow.h
  54. 2 0
      src/widgets/widgets.pri
  55. 29 0
      tests/commonfull.pri
  56. 1 22
      tests/test_core/test_notebook/test_notebook.pro
  57. 77 0
      tests/test_task/test_task.cpp
  58. 31 0
      tests/test_task/test_task.h
  59. 10 0
      tests/test_task/test_task.pro
  60. 64 0
      tests/test_utils/test_utils.cpp
  61. 12 0
      tests/test_utils/test_utils.h
  62. 2 0
      tests/test_utils/test_utils.pro
  63. 2 1
      tests/tests.pro

+ 30 - 13
src/core/configmgr.cpp

@@ -36,6 +36,10 @@ const QString ConfigMgr::c_configFileName = "vnotex.json";
 
 const QString ConfigMgr::c_sessionFileName = "session.json";
 
+const QString ConfigMgr::c_userFilesFolder = "user_files";
+
+const QString ConfigMgr::c_appFilesFolder = "vnotex_files";
+
 const QJsonObject &ConfigMgr::Settings::getJson() const
 {
     return m_jobj;
@@ -68,17 +72,21 @@ ConfigMgr::ConfigMgr(bool p_isUnitTest, QObject *p_parent)
             qWarning() << "failed to init ConfigMgr for UnitTest";
             return;
         }
-        m_appConfigFolderPath = m_dirForUnitTest->filePath("vnotex_files");
-        m_userConfigFolderPath = m_dirForUnitTest->filePath("user_files");
 
-        FileUtils::copyFile(getConfigFilePath(Source::Default), PathUtils::concatenateFilePath(m_appConfigFolderPath, c_configFileName));
-    } else {
-        locateConfigFolder();
+        QDir dir(m_dirForUnitTest->path());
+        dir.mkdir(c_appFilesFolder);
+        dir.mkdir(c_userFilesFolder);
 
-        bool needUpdate = checkAppConfig();
-        if (needUpdate) {
-            checkUserConfig();
-        }
+        m_appConfigFolderPath = m_dirForUnitTest->filePath(c_appFilesFolder);
+        m_userConfigFolderPath = m_dirForUnitTest->filePath(c_userFilesFolder);
+        return;
+    }
+
+    locateConfigFolder();
+
+    bool needUpdate = checkAppConfig();
+    if (needUpdate) {
+        checkUserConfig();
     }
 
     m_config->init();
@@ -106,8 +114,7 @@ void ConfigMgr::locateConfigFolder()
     qInfo() << "app folder" << appDirPath;
     // Check app config.
     {
-        const QString configFolderName("vnotex_files");
-        QString folderPath(appDirPath + '/' + configFolderName);
+        QString folderPath(appDirPath + '/' + c_appFilesFolder);
         if (QDir(folderPath).exists()) {
             // Config folder in app/.
             m_appConfigFolderPath = PathUtils::cleanPath(folderPath);
@@ -118,8 +125,7 @@ void ConfigMgr::locateConfigFolder()
 
     // Check user config.
     {
-        const QString configFolderName("user_files");
-        QString folderPath(appDirPath + '/' + configFolderName);
+        QString folderPath(appDirPath + '/' + c_userFilesFolder);
         if (QDir(folderPath).exists()) {
             // Config folder in app/.
             m_userConfigFolderPath = PathUtils::cleanPath(folderPath);
@@ -564,3 +570,14 @@ QString ConfigMgr::getApplicationVersion()
 
     return appVersion;
 }
+
+QJsonValue ConfigMgr::parseAndReadConfig(const QString &p_exp) const
+{
+    if (p_exp.startsWith(QStringLiteral("main."))) {
+        return Utils::parseAndReadJson(m_config->toJson(), p_exp.mid(5));
+    } else if (p_exp.startsWith(QStringLiteral("session."))) {
+        return Utils::parseAndReadJson(m_sessionConfig->toJson(), p_exp.mid(8));
+    } else {
+        return QJsonValue();
+    }
+}

+ 7 - 0
src/core/configmgr.h

@@ -106,6 +106,9 @@ namespace vnotex
 
         QString getConfigFilePath(Source p_src) const;
 
+        // Parse exp like "[main|session].core.shortcuts.FullScreen" and return the config value.
+        QJsonValue parseAndReadConfig(const QString &p_exp) const;
+
         // Called at boostrap without QApplication instance.
         static QString locateSessionConfigFilePathAtBootstrap();
 
@@ -168,6 +171,10 @@ namespace vnotex
 
         // Name of the session config file.
         static const QString c_sessionFileName;
+
+        static const QString c_userFilesFolder;
+
+        static const QString c_appFilesFolder;
     };
 } // ns vnotex
 

+ 0 - 10
src/core/core.pri

@@ -29,11 +29,6 @@ SOURCES += \
     $$PWD/texteditorconfig.cpp \
     $$PWD/vnotex.cpp \
     $$PWD/thememgr.cpp \
-    $$PWD/task.cpp \
-    $$PWD/taskhelper.cpp \
-    $$PWD/taskmgr.cpp \
-    $$PWD/taskvariablemgr.cpp \
-    $$PWD/shellexecution.cpp \
     $$PWD/notebookmgr.cpp \
     $$PWD/theme.cpp \
     $$PWD/sessionconfig.cpp \
@@ -65,11 +60,6 @@ HEADERS += \
     $$PWD/texteditorconfig.h \
     $$PWD/vnotex.h \
     $$PWD/thememgr.h \
-    $$PWD/task.h \
-    $$PWD/taskhelper.h \
-    $$PWD/taskmgr.h \
-    $$PWD/taskvariablemgr.h \
-    $$PWD/shellexecution.h \
     $$PWD/global.h \
     $$PWD/namebasedserver.h \
     $$PWD/exception.h \

+ 5 - 1
src/core/editorconfig.cpp

@@ -150,7 +150,11 @@ QJsonObject EditorConfig::toJson() const
     obj[m_markdownEditorConfig->getSessionName()] = m_markdownEditorConfig->toJson();
     obj[QStringLiteral("core")] = saveCore();
     obj[QStringLiteral("image_host")] = saveImageHost();
-    obj[QStringLiteral("vi")] = m_viConfig->toJson();
+
+    // In UT, it may be nullptr.
+    if (m_viConfig) {
+        obj[QStringLiteral("vi")] = m_viConfig->toJson();
+    }
     return obj;
 }
 

+ 5 - 0
src/core/notebookmgr.cpp

@@ -265,6 +265,11 @@ ID NotebookMgr::getCurrentNotebookId() const
     return m_currentNotebookId;
 }
 
+QSharedPointer<Notebook> NotebookMgr::getCurrentNotebook() const
+{
+    return findNotebookById(m_currentNotebookId);
+}
+
 void NotebookMgr::setCurrentNotebook(ID p_notebookId)
 {
     auto lastId = m_currentNotebookId;

+ 2 - 0
src/core/notebookmgr.h

@@ -62,6 +62,8 @@ namespace vnotex
 
         ID getCurrentNotebookId() const;
 
+        QSharedPointer<Notebook> getCurrentNotebook() const;
+
         // Find the notebook with the same directory as root folder.
         QSharedPointer<Notebook> findNotebookByRootFolderPath(const QString &p_rootFolderPath) const;
 

+ 0 - 102
src/core/shellexecution.cpp

@@ -1,102 +0,0 @@
-#include "shellexecution.h"
-
-#include <QFileInfo>
-
-using namespace vnotex;
-
-ShellExecution::ShellExecution()
-{
-    auto shell = defaultShell();
-    setShellExecutable(shell);
-    setShellArguments(defaultShellArguments(shell));
-}
-
-void ShellExecution::setProgram(const QString &p_prog)
-{
-    m_program = p_prog;
-}
-
-void ShellExecution::setArguments(const QStringList &p_args)
-{
-    m_args = p_args;
-}
-
-void ShellExecution::setShellExecutable(const QString &p_exec)
-{
-    m_shell_executable = p_exec;
-}
-
-void ShellExecution::setShellArguments(const QStringList &p_args)
-{
-    m_shell_args = p_args;
-}
-
-void ShellExecution::setupProcess(QProcess *p_process, 
-                                  const QString &p_program,
-                                  const QStringList &p_args, 
-                                  const QString &p_shell_exec, 
-                                  const QStringList &p_shell_args)
-{
-    auto shell_exec = p_shell_exec.isNull() ? defaultShell() : p_shell_exec;
-    auto shell_args = p_shell_args.isEmpty() ? defaultShellArguments(shell_exec)
-                                             : p_shell_args;
-    p_process->setProgram(shell_exec);
-    auto args = p_args;
-    
-    auto shell = shellBasename(shell_exec);
-    if (!p_program.isEmpty() && !p_args.isEmpty()) {
-        args = shellQuote(args, shell_exec);
-    }
-    QStringList allArgs(shell_args);
-    if (shell == "bash") {
-        allArgs << (QStringList() << p_program << args).join(' ');
-    } else {
-        allArgs << p_program << args;
-    }
-    p_process->setArguments(allArgs);
-}
-
-QString ShellExecution::shellBasename(const QString &p_shell)
-{
-    return QFileInfo(p_shell).baseName().toLower();    
-}
-
-QString ShellExecution::defaultShell()
-{
-#ifdef Q_OS_WIN
-    return "PowerShell.exe";
-#else
-    return "/bin/bash";
-#endif
-}
-
-QStringList ShellExecution::defaultShellArguments(const QString &p_shell)
-{
-    auto shell = shellBasename(p_shell);
-    if (shell == "cmd") {
-        return {"/C"};
-    } else if (shell == "powershell" || p_shell == "pwsh") {
-        return {"-Command"};
-    } else if (shell == "bash") {
-        return {"-c"};
-    }
-    return {};
-}
-
-QString ShellExecution::shellQuote(const QString &p_text, const QString &)
-{
-    if (p_text.contains(' ')) {
-        return QString("\"%1\"").arg(p_text);
-    }
-    return p_text;
-}
-
-QStringList ShellExecution::shellQuote(const QStringList &p_list, const QString &p_shell)
-{
-    auto shell = shellBasename(p_shell);
-    QStringList list;
-    for (const auto &s : p_list) {
-        list << shellQuote(s, shell);
-    }
-    return list;
-}

+ 0 - 42
src/core/shellexecution.h

@@ -1,42 +0,0 @@
-#ifndef SHELLEXECUTION_H
-#define SHELLEXECUTION_H
-
-#include <QProcess>
-
-namespace vnotex {
-
-
-class ShellExecution : public QProcess
-{
-    Q_OBJECT
-public:
-    ShellExecution();
-    void setProgram(const QString &p_prog);
-    void setArguments(const QStringList &p_args);
-    void setShellExecutable(const QString &p_exec);
-    void setShellArguments(const QStringList &p_args);
-    
-    static void setupProcess(QProcess *p_process,
-                             const QString &p_program,
-                             const QStringList &p_args = QStringList(),
-                             const QString &p_shell_exec = QString(),
-                             const QStringList &p_shell_args = QStringList());
-    static QString defaultShell();
-    static QStringList defaultShellArguments(const QString &p_shell);
-    static QString shellQuote(const QString &p_text,
-                              const QString &p_shell);
-    static QStringList shellQuote(const QStringList &p_list,
-                                  const QString &p_shell);
-    
-private:
-    QString m_shell_executable;
-    QStringList m_shell_args;
-    QString m_program;
-    QStringList m_args;
-    
-    static QString shellBasename(const QString &p_shell);
-};
-
-}
-
-#endif // SHELLEXECUTION_H

+ 0 - 557
src/core/task.cpp

@@ -1,557 +0,0 @@
-#include "task.h"
-
-#include <QJsonDocument>
-#include <QVersionNumber>
-#include <QDebug>
-#include <QJsonValue>
-#include <QJsonArray>
-#include <QAction>
-#include <QRegularExpression>
-#include <QInputDialog>
-#include <QTextCodec>
-#include <QRandomGenerator>
-
-#include "utils/fileutils.h"
-#include "utils/pathutils.h"
-#include "vnotex.h"
-#include "exception.h"
-#include "taskhelper.h"
-#include "notebook/notebook.h"
-#include "shellexecution.h"
-
-
-using namespace vnotex;
-
-QString Task::s_latestVersion = "0.1.3";
-TaskVariableMgr Task::s_vars;
-
-Task *Task::fromFile(const QString &p_file, 
-                     const QJsonDocument &p_json,
-                     const QString &p_locale, 
-                     QObject *p_parent)
-{
-    auto task = new Task(p_locale, p_file, p_parent);
-    return fromJson(task, p_json.object());
-}
-
-Task* Task::fromJson(Task *p_task,
-                     const QJsonObject &p_obj)
-{
-    if (p_obj.contains("version")) {
-        p_task->dto.version = p_obj["version"].toString();
-    }
-    
-    auto version = QVersionNumber::fromString(p_task->getVersion());
-    if (version < QVersionNumber(1, 0, 0)) {
-        return fromJsonV0(p_task, p_obj);
-    }
-    qWarning() << "Unknow Task Version" << version << p_task->dto._source;
-    return p_task;
-}
-
-Task *Task::fromJsonV0(Task *p_task,
-                       const QJsonObject &p_obj,
-                       bool mergeTasks)
-{
-    if (p_obj.contains("type")) {
-        p_task->dto.type = p_obj["type"].toString();
-    }
-    
-    if (p_obj.contains("icon")) {
-        QString path = p_obj["icon"].toString();
-        QDir iconPath(path);
-        if (iconPath.isRelative()) {
-            QDir taskDir(p_task->dto._source);
-            taskDir.cdUp();
-            path = QDir(taskDir.filePath(path)).absolutePath();
-        }
-        if (QFile::exists(path)) {
-            p_task->dto.icon = path;
-        } else {
-            qWarning() << "task icon not exists" << path;
-        }
-    }
-    
-    if (p_obj.contains("shortcut")) {
-        p_task->dto.shortcut = p_obj["shortcut"].toString();
-    }
-    
-    if (p_obj.contains("type")) {
-        p_task->dto.type = p_obj["type"].toString();
-    }
-    
-    if (p_obj.contains("command")) {
-        p_task->dto.command = getLocaleString(p_obj["command"], p_task->m_locale);
-    }
-    
-    if (p_obj.contains("args")) {
-        p_task->dto.args = getLocaleStringList(p_obj["args"], p_task->m_locale);
-    }
-    
-    if (p_obj.contains("label")) {
-        p_task->dto.label = getLocaleString(p_obj["label"], p_task->m_locale);
-    } else if (p_task->dto.label.isNull() && !p_task->dto.command.isNull()) {
-        p_task->dto.label = p_task->dto.command;
-    }
-    
-    if (p_obj.contains("options")) {
-        auto options = p_obj["options"].toObject();
-        
-        if (options.contains("cwd")) {
-            p_task->dto.options.cwd = options["cwd"].toString();
-        }
-        
-        if (options.contains("env")) {
-            p_task->dto.options.env.clear();
-            auto env = options["env"].toObject();
-            for (auto i = env.begin(); i != env.end(); i++) {
-                auto key = i.key();
-                auto value = getLocaleString(i.value(), p_task->m_locale);
-                p_task->dto.options.env.insert(key, value);
-            }
-        }
-        
-        if (options.contains("shell") && p_task->getType() == "shell") {
-            auto shell = options["shell"].toObject();
-            
-            if (shell.contains("executable")) {
-                p_task->dto.options.shell.executable = shell["executable"].toString();
-            }
-            
-            if (shell.contains("args")) {
-                p_task->dto.options.shell.args.clear();
-                
-                for (auto arg : shell["args"].toArray()) {
-                    p_task->dto.options.shell.args << arg.toString();
-                }
-            }
-        }
-    }
-    
-    if (p_obj.contains("tasks")) {
-        if (!mergeTasks) p_task->m_tasks.clear();
-        auto tasks = p_obj["tasks"].toArray();
-        
-        for (const auto &task : tasks) {
-            auto t = new Task(p_task->m_locale,
-                              p_task->getFile(),
-                              p_task);
-            p_task->m_tasks.append(fromJson(t, task.toObject()));
-        }
-    }
-    
-    if (p_obj.contains("inputs")) {
-        p_task->dto.inputs.clear();
-        auto inputs = p_obj["inputs"].toArray();
-        
-        for (const auto &input : inputs) {
-            auto in = input.toObject();
-            InputDTO i;
-            if (in.contains("id")) {
-                i.id = in["id"].toString();
-            } else {
-                qWarning() << "Input configuration not contain id";
-            }
-            
-            if (in.contains("type")) {
-                i.type = in["type"].toString();
-            } else {
-                i.type = "promptString";
-            }
-            
-            if (in.contains("description")) {
-                i.description = getLocaleString(in["description"], p_task->m_locale);
-            }
-            
-            if (in.contains("default")) {
-                i.default_ = getLocaleString(in["default"], p_task->m_locale);
-            }
-            
-            if (i.type == "promptString" && in.contains("password")) {
-                i.password = in["password"].toBool();
-            } else {
-                i.password = false;
-            }
-            
-            if (i.type == "pickString" && in.contains("options")) {
-                i.options = getLocaleStringList(in["options"], p_task->m_locale);
-            }
-            
-            if (i.type == "pickString" && !i.default_.isNull() && !i.options.contains(i.default_)) {
-                qWarning() << "default must be one of the option values";
-            }
-            
-            p_task->dto.inputs << i;
-        }
-    }
-    
-    if (p_obj.contains("messages")) {
-        p_task->dto.messages.clear();
-        auto messages = p_obj["messages"].toArray();
-        
-        for (const auto &message : messages) {
-            auto msg = message.toObject();
-            MessageDTO m;
-            if (msg.contains("id")) {
-                m.id = msg["id"].toString();
-            } else {
-                qWarning() << "Message configuration not contain id";
-            }
-            
-            if (msg.contains("type")) {
-                m.type = msg["type"].toString();
-            } else {
-                m.type = "information";
-            }
-            
-            if (msg.contains("title")) {
-                m.title = getLocaleString(msg["title"], p_task->m_locale);
-            }
-            
-            if (msg.contains("text")) {
-                m.text = getLocaleString(msg["text"], p_task->m_locale);
-            }
-            
-            if (msg.contains("detailedText")) {
-                m.detailedText = getLocaleString(msg["detailedText"], p_task->m_locale);
-            }
-            
-            if (msg.contains("buttons")) {
-                auto buttons = msg["buttons"].toArray();
-                for (auto button : buttons) {
-                    auto btn = button.toObject();
-                    ButtonDTO b;
-                    b.text = getLocaleString(btn["text"], p_task->m_locale);
-                    m.buttons << b;
-                }
-            }
-            p_task->dto.messages << m;
-        }
-    }
-    
-    // OS-specific task configuration
-#if defined (Q_OS_WIN)
-#define OS_SPEC "windows"
-#endif
-#if defined (Q_OS_MACOS)
-#define OS_SPEC "osx"
-#endif
-#if defined (Q_OS_LINUX)
-#define OS_SPEC "linux"
-#endif
-    if (p_obj.contains(OS_SPEC)) {
-        auto os = p_obj[OS_SPEC].toObject();
-        fromJsonV0(p_task, os, true);
-    }
-#undef OS_SPEC
-
-    return p_task;
-}
-
-QString Task::getVersion() const
-{
-    return dto.version;
-}
-
-QString Task::getType() const
-{
-    return dto.type;
-}
-
-QString Task::getCommand() const
-{
-    return s_vars.evaluate(dto.command, this);
-}
-
-QStringList Task::getArgs() const
-{
-    return s_vars.evaluate(dto.args, this);
-}
-
-QString Task::getLabel() const
-{
-    return dto.label;
-}
-
-QString Task::getIcon() const
-{
-    return dto.icon;
-}
-
-QString Task::getShortcut() const
-{
-    return dto.shortcut;
-}
-
-QString Task::getOptionsCwd() const
-{
-    auto cwd = dto.options.cwd;
-    if (!cwd.isNull()) {
-        return s_vars.evaluate(cwd, this);
-    }
-    auto notebook = TaskHelper::getCurrentNotebook();
-    if (notebook) cwd = notebook->getRootFolderAbsolutePath();
-    if (!cwd.isNull()) {
-        return cwd;
-    }
-    cwd = TaskHelper::getCurrentFile();
-    if (!cwd.isNull()) {
-        return QFileInfo(cwd).dir().absolutePath();
-    }
-    return QFileInfo(dto._source).dir().absolutePath();
-}
-
-const QMap<QString, QString> &Task::getOptionsEnv() const
-{
-    return dto.options.env;
-}
-
-QString Task::getOptionsShellExecutable() const
-{
-    return dto.options.shell.executable;
-}
-
-QStringList Task::getOptionsShellArgs() const
-{
-    if (dto.options.shell.args.isEmpty()) {
-        return ShellExecution::defaultShellArguments(dto.options.shell.executable);
-    } else {
-        return s_vars.evaluate(dto.options.shell.args, this);
-    }
-}    
-
-const QVector<Task *> &Task::getTasks() const
-{
-    return m_tasks;
-}
-
-const QVector<InputDTO> &Task::getInputs() const
-{
-    return dto.inputs;
-}
-
-InputDTO Task::getInput(const QString &p_id) const
-{
-    for (auto i : dto.inputs) {
-        if (i.id == p_id) {
-            return i;
-        }
-    }
-    qDebug() << getLabel();
-    qWarning() << "input" << p_id << "not found";
-    throw "Input variable can not found";
-}
-
-MessageDTO Task::getMessage(const QString &p_id) const
-{
-    for (auto msg : dto.messages) {
-        if (msg.id == p_id) {
-            return msg;
-        }
-    }
-    qDebug() << getLabel();
-    qWarning() << "message" << p_id << "not found";
-    throw "Message can not found";
-}
-
-QString Task::getFile() const
-{
-    return dto._source;
-}
-
-Task::Task(const QString &p_locale, 
-           const QString &p_file,
-           QObject *p_parent)
-    : QObject(p_parent)
-{   
-    dto._source = p_file;
-    dto.version = s_latestVersion;
-    dto.type = "shell";
-    dto.options.shell.executable = ShellExecution::defaultShell();
-    
-    // inherit configuration
-    m_parent = qobject_cast<Task*>(p_parent);
-    if (m_parent) {
-        dto.version = m_parent->dto.version;
-        dto.type = m_parent->dto.type;
-        dto.command = m_parent->dto.command;
-        dto.args = m_parent->dto.args;
-        dto.options.cwd = m_parent->dto.options.cwd;
-        dto.options.env = m_parent->dto.options.env;
-        dto.options.shell.executable = m_parent->dto.options.shell.executable;
-        dto.options.shell.args = m_parent->dto.options.shell.args;
-        // not inherit label/inputs/tasks
-    } else {
-        dto.label = QFileInfo(p_file).baseName();
-    }
-    
-    if (!p_locale.isNull()) {
-        m_locale = p_locale;
-    }
-}
-
-QProcess *Task::setupProcess() const
-{
-    // Set process property
-    auto command = getCommand();
-    if (command.isEmpty()) return nullptr;
-    auto process = new QProcess(this->parent());
-    process->setWorkingDirectory(getOptionsCwd());
-    
-    auto options_env = getOptionsEnv();
-    if (!options_env.isEmpty()) {
-        auto env = QProcessEnvironment::systemEnvironment();
-        for (auto i = options_env.begin(); i != options_env.end(); i++) {
-            env.insert(i.key(), i.value());
-        }
-        process->setProcessEnvironment(env);
-    }
-    
-    auto args = getArgs();
-    auto type = getType();
-    
-    // set program and args
-    if (type == "shell") {
-        ShellExecution::setupProcess(process,
-                                     command,
-                                     args,
-                                     getOptionsShellExecutable(),
-                                     getOptionsShellArgs());
-    } else if (getType() == "process") {
-        process->setProgram(command);
-        process->setArguments(args);
-    }
-
-    // connect signal and slot
-    connect(process, &QProcess::started,
-            this, [this]() {
-        emit showOutput(tr("[Task %1 started]\n").arg(getLabel()));
-    });
-    connect(process, &QProcess::readyReadStandardOutput,
-            this, [process, this]() {
-        auto text = textDecode(process->readAllStandardOutput());
-        text = TaskHelper::handleCommand(text, process, this);
-        emit showOutput(text);
-    });
-    connect(process, &QProcess::readyReadStandardError,
-            this, [process, this]() {
-        auto text = process->readAllStandardError();
-        emit showOutput(textDecode(text));
-    });
-    connect(process, &QProcess::errorOccurred,
-            this, [this](QProcess::ProcessError error) {
-        emit showOutput(tr("[Task %1 error occurred with code %2]\n").arg(getLabel(), QString::number(error)));
-    });
-    connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
-            this, [this, process](int exitCode) {
-        emit showOutput(tr("\n[Task %1 finished with exit code %2]\n")
-                        .arg(getLabel(), QString::number(exitCode)));
-        process->deleteLater();
-    });
-    return process;
-}
-
-void Task::run() const
-{
-    QProcess *process;
-    try {
-        process = setupProcess();
-    }  catch (const char *msg) {
-        qDebug() << msg;
-        return ;
-    }
-    if (process) {
-        // start process
-        qInfo() << "run task" << process->program() << process->arguments();
-        process->start();
-    }
-}
-
-TaskDTO Task::getDTO() const
-{
-    return dto;
-}
-
-QString Task::textDecode(const QByteArray &p_text)
-{
-    static QByteArrayList codecNames = {
-        "UTF-8",
-        "System",
-        "UTF-16",
-        "GB18030"
-    };
-    for (auto name : codecNames) {
-        auto text = textDecode(p_text, name);
-        if (!text.isNull()) return text;
-    }
-    return p_text;
-}
-
-QString Task::textDecode(const QByteArray &p_text, const QByteArray &name)
-{
-    auto codec = QTextCodec::codecForName(name);
-    if (codec) {
-        QTextCodec::ConverterState state;
-        auto text = codec->toUnicode(p_text.data(), p_text.size(), &state);
-        if (state.invalidChars > 0) return QString();
-        return text;
-    }
-    return QString();
-}
-
-
-bool Task::isValidTaskFile(const QString &p_file, 
-                           QJsonDocument &p_json)
-{
-    QFile file(p_file);
-    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
-        return false;
-    }
-    QJsonParseError error;
-    p_json = QJsonDocument::fromJson(file.readAll(), &error);
-    file.close();
-    if (p_json.isNull()) {
-        qDebug() << "load task" << p_file << "failed: " << error.errorString()
-                 << "at offset" << error.offset;
-        return false;
-    }
-    
-    return true;
-}
-
-QString Task::getLocaleString(const QJsonValue &p_value, const QString &p_locale)
-{
-    if (p_value.isObject()) {
-        auto obj = p_value.toObject();
-        if (obj.contains(p_locale)) {
-            return obj.value(p_locale).toString();
-        } else {
-            qWarning() << "current locale" << p_locale << "not found";
-            if (!obj.isEmpty()){
-                return obj.begin().value().toString();
-            } else {
-                return QString();
-            }
-        }
-    } else {
-        return p_value.toString();
-    }
-}
-
-QStringList Task::getLocaleStringList(const QJsonValue &p_value, const QString &p_locale)
-{
-    QStringList list;
-    for (auto value : p_value.toArray()) {
-        list << getLocaleString(value, p_locale);
-    }
-    return list;
-}
-
-QStringList Task::getStringList(const QJsonValue &p_value)
-{
-    QStringList list;
-    for (auto value : p_value.toArray()) {
-        list << value.toString();
-    }
-    return list;
-}

+ 0 - 185
src/core/task.h

@@ -1,185 +0,0 @@
-#ifndef TASK_H
-#define TASK_H
-
-#include <QObject>
-#include <QJsonObject>
-#include <QVector>
-#include <QMap>
-#include <QProcess>
-
-#include "taskvariablemgr.h"
-
-class QAction;
-
-namespace vnotex {
-
-    struct ButtonDTO {
-        QString text;
-    };
-
-    struct InputDTO {
-        QString id;
-
-        QString type;
-
-        QString description;
-
-        QString default_;
-
-        bool password;
-
-        QStringList options;
-    };
-
-    struct MessageDTO {
-        QString id;
-
-        QString type;
-
-        QString title;
-
-        QString text;
-
-        QString detailedText;
-
-        QVector<ButtonDTO> buttons;
-    };
-
-    struct ShellOptionsDTO {
-        QString executable;
-
-        QStringList args;
-    };
-
-    struct TaskOptionsDTO {
-        QString cwd;
-
-        QMap<QString, QString> env;
-
-        ShellOptionsDTO shell;
-    };
-
-    struct TaskDTO {
-        QString version;
-
-        QString type;
-
-        QString command;
-
-        QStringList args;
-
-        QString label;
-
-        QString icon;
-
-        QString shortcut;
-
-        QVector<InputDTO> inputs;
-
-        QVector<MessageDTO> messages;
-
-        TaskOptionsDTO options;
-
-        QString _scope;
-
-        QString _source;
-    };
-
-    class Notebook;
-
-    class Task : public QObject
-    {
-        Q_OBJECT
-    public:
-
-
-        static Task* fromFile(const QString &p_file,
-                              const QJsonDocument &p_json,
-                              const QString &p_locale,
-                              QObject *p_parent = nullptr);
-
-        void run() const;
-
-        TaskDTO getDTO() const;
-
-        QString getVersion() const;
-
-        QString getType() const;
-
-        QString getCommand() const;
-
-        QStringList getArgs() const;
-
-        QString getLabel() const;
-
-        QString getIcon() const;
-
-        QString getShortcut() const;
-
-        QString getOptionsCwd() const;
-
-        const QMap<QString, QString> &getOptionsEnv() const;
-
-        QString getOptionsShellExecutable() const;
-
-        QStringList getOptionsShellArgs() const;
-
-        const QVector<Task*> &getTasks() const;
-
-        const QVector<InputDTO> &getInputs() const;
-
-        InputDTO getInput(const QString &p_id) const;
-
-        MessageDTO getMessage(const QString &p_id) const;
-
-        QString getFile() const;
-
-        static QString s_latestVersion;
-
-        static bool isValidTaskFile(const QString &p_file,
-                                    QJsonDocument &p_json);
-
-        static QString getLocaleString(const QJsonValue &p_value,
-                                       const QString &p_locale);
-
-        static QStringList getLocaleStringList(const QJsonValue &p_value,
-                                          const QString &p_locale);
-
-        static QStringList getStringList(const QJsonValue &p_value);
-
-    signals:
-        void showOutput(const QString &p_text) const;
-
-    private:
-        static Task* fromJson(Task *p_task,
-                              const QJsonObject &p_obj);
-
-        static Task* fromJsonV0(Task *p_task,
-                                const QJsonObject &p_obj,
-                                bool mergeTasks = false);
-
-        explicit Task(const QString &p_locale,
-                            const QString &p_file = QString(),
-                            QObject *p_parent = nullptr);
-
-        QProcess *setupProcess() const;
-
-        static QString textDecode(const QByteArray &p_text);
-
-        static QString textDecode(const QByteArray &p_text, const QByteArray &name);
-
-        TaskDTO dto;
-
-        Task *m_parent = nullptr;
-
-        QVector<Task*> m_tasks;
-
-        QString m_locale;
-
-        static TaskVariableMgr s_vars;
-
-    };
-
-} // ns vnotex
-
-#endif // TASK_H

+ 0 - 233
src/core/taskhelper.cpp

@@ -1,233 +0,0 @@
-#include "taskhelper.h"
-
-#include <QRegularExpression>
-#include <QJsonArray>
-#include <QMessageBox>
-#include <QPushButton>
-#include <QInputDialog>
-
-#include "vnotex.h"
-#include "notebookmgr.h"
-#include <widgets/mainwindow.h>
-#include <widgets/viewarea.h>
-#include <widgets/viewwindow.h>
-#include <widgets/dialogs/selectdialog.h>
-#include <widgets/markdownviewwindow.h>
-#include <widgets/textviewwindow.h>
-#include <utils/pathutils.h>
-
-using namespace vnotex;
-
-QString TaskHelper::normalPath(const QString &p_text)
-{
-#if defined (Q_OS_WIN)
-    return QString(p_text).replace('/', '\\');
-#endif
-    return p_text; 
-}
-
-QString TaskHelper::getCurrentFile()
-{
-    auto win = VNoteX::getInst().getMainWindow()->getViewArea()->getCurrentViewWindow();
-    QString file;
-    if (win && win->getBuffer()) {
-        file = win->getBuffer()->getPath();
-    }
-    return file;
-}
-
-QSharedPointer<Notebook> TaskHelper::getCurrentNotebook()
-{
-    const auto &notebookMgr = VNoteX::getInst().getNotebookMgr();
-    auto id = notebookMgr.getCurrentNotebookId();
-    if (id == Notebook::InvalidId) return nullptr;
-    return notebookMgr.findNotebookById(id);
-}
-
-QString TaskHelper::getFileNotebookFolder(const QString p_currentFile)
-{
-    const auto &notebookMgr = VNoteX::getInst().getNotebookMgr();
-    const auto &notebooks = notebookMgr.getNotebooks();
-    for (auto notebook : notebooks) {
-        auto rootPath = notebook->getRootFolderAbsolutePath();
-        if (PathUtils::pathContains(rootPath, p_currentFile)) {
-            return rootPath;
-        }
-    }
-    return QString();
-}
-
-QString TaskHelper::getSelectedText()
-{
-    auto window = VNoteX::getInst().getMainWindow()->getViewArea()->getCurrentViewWindow();
-    {
-        auto win = dynamic_cast<MarkdownViewWindow*>(window);
-        if (win) {
-            return win->selectedText();
-        }
-    }
-    {
-        auto win = dynamic_cast<TextViewWindow*>(window);
-        if (win) {
-            return win->selectedText();
-        }
-    }
-    return QString();
-}
-
-QStringList TaskHelper::getAllSpecialVariables(const QString &p_name, const QString &p_text)
-{
-    QStringList list;
-    QRegularExpression re(QString(R"(\$\{[\t ]*%1[\t ]*:[\t ]*(.*?)[\t ]*\})").arg(p_name));
-    auto it = re.globalMatch(p_text);
-    while (it.hasNext()) {
-        list << it.next().captured(1);
-    }
-    return list;
-}
-
-QString TaskHelper::replaceAllSepcialVariables(const QString &p_name, 
-                                               const QString &p_text, 
-                                               const QMap<QString, QString> &p_map)
-{
-    auto text = p_text;
-    for (auto i = p_map.begin(); i != p_map.end(); i++) {
-        auto key = QString(i.key()).replace(".", "\\.").replace("[", "\\[").replace("]", "\\]");
-        auto pattern = QRegularExpression(QString(R"(\$\{[\t ]*%1[\t ]*:[\t ]*%2[\t ]*\})").arg(p_name, key));
-        text = text.replace(pattern, i.value());
-    }
-    return text;
-}
-
-QString TaskHelper::evaluateJsonExpr(const QJsonObject &p_obj, const QString &p_expr)
-{
-    QJsonValue value = p_obj;
-    for (auto token : p_expr.split('.')) {
-        auto pos = token.indexOf('[');
-        auto name = token.mid(0, pos);
-        value = value.toObject().value(name);
-        if (pos == -1) continue;
-        if (token.back() == ']') {
-            for (auto idx : token.mid(pos+1, token.length()-pos-2).split("][")) {
-                bool ok;
-                auto index = idx.toInt(&ok);
-                if (!ok) throw "Config variable syntax error!";
-                value = value.toArray().at(index);
-            }
-        } else {
-            throw "Config variable syntax error!";
-        }
-    }
-    if (value.isBool()) {
-        if (value.toBool()) return "true";
-        else return "false";
-    } else if (value.isDouble()) {
-        return QString::number(value.toDouble());
-    } else if (value.isNull()) {
-        return "null";
-    } else if (value.isUndefined()) {
-        return "undefined";
-    }
-    return value.toString();
-}
-
-QString TaskHelper::getPathSeparator()
-{
-#if defined (Q_OS_WIN)
-    return "\\";
-#else
-    return "/";
-#endif
-}
-
-QString TaskHelper::handleCommand(const QString &p_text, 
-                                  QProcess *p_process,
-                                  const Task *p_task)
-{
-    QRegularExpression re(R"(^::([a-zA-Z-]+)(.*?)?::(.*?)$)", 
-                          QRegularExpression::MultilineOption);
-    auto i = re.globalMatch(p_text);
-    while (i.hasNext()) {
-        auto match = i.next();
-        auto cmd = match.captured(1).toLower();
-        auto args = match.captured(2).trimmed().split(',');
-        auto value = match.captured(3);
-        
-        QMap<QString, QString> arg;
-        for (const auto &i : args) {
-            auto s = i.trimmed();
-            auto p = s.indexOf('=');
-            auto name = s.mid(0, p);
-            QString val;
-            if (p != -1) {
-                val = s.mid(p+1);
-            }
-            arg.insert(name, val);
-        }
-        if (cmd == "show-message") {
-            QMessageBox box;
-            // fill message dto
-            auto msgId = arg.value("id");
-            QVector<QPushButton*> buttons;
-            if (!msgId.isEmpty()) {
-                MessageDTO msgd = p_task->getMessage(msgId);
-                box.setWindowTitle(msgd.title);
-                box.setText(msgd.text);
-                box.setDetailedText(msgd.detailedText);
-                for (auto button : msgd.buttons) {
-                    buttons.append(box.addButton(button.text, QMessageBox::ActionRole));
-                }
-            }
-            // fill args
-            if (arg.contains("title")) box.setWindowTitle(arg["title"]);
-            if (arg.contains("text")) box.setText(arg["text"]);
-            if (arg.contains("detailedText")) box.setWindowTitle(arg["detailedText"]);
-            if (arg.contains("buttons")) {
-                buttons.clear();
-                for (auto button : arg["buttons"].split('|')) {
-                    buttons.append(box.addButton(button, QMessageBox::ActionRole));
-                }
-            }
-            box.exec();
-            int clickedBtnId;
-            for (clickedBtnId = 0; clickedBtnId < buttons.size(); clickedBtnId++) {
-                if (box.clickedButton() == buttons.at(clickedBtnId)) {
-                    break;
-                }
-            }
-            if (p_process) {
-                if (p_process->state() == QProcess::Running) {
-                    p_process->write(QByteArray::number(clickedBtnId)+"\n");
-                }
-            } else {
-                qWarning() << "process finished!";
-            }
-        } else if (cmd == "show-inputdialog") {
-            QInputDialog dialog;
-            dialog.setWindowTitle(arg.value("title"));
-            
-        } else if (cmd == "show-info") {
-            QMessageBox::information(VNoteX::getInst().getMainWindow(),
-                                     arg.value("title"),
-                                     value);
-        } else if (cmd == "show-question") {
-            auto ret = QMessageBox::question(VNoteX::getInst().getMainWindow(),
-                                             arg.value("title"),
-                                             value);
-            if (p_process) {
-                if (p_process->state() == QProcess::Running) {
-                    p_process->write(QByteArray::number(ret)+"\n");
-                }
-            } else {
-                qWarning() << "process finished!";
-            }
-        }
-    }
-    auto text = p_text;
-    return text.replace(re, "");
-}
-
-TaskHelper::TaskHelper()
-{
-    
-}

+ 0 - 50
src/core/taskhelper.h

@@ -1,50 +0,0 @@
-#ifndef TASKHELPER_H
-#define TASKHELPER_H
-
-#include <QString>
-#include <QSharedPointer>
-
-class QProcess;
-
-namespace vnotex {
-
-class Notebook;
-class Task;
-
-class TaskHelper
-{
-public:
-    // helper functions
-    
-    static QString normalPath(const QString &p_text);
-
-    static QString getCurrentFile();
-    
-    static QSharedPointer<Notebook> getCurrentNotebook();
-    
-    static QString getFileNotebookFolder(const QString p_currentFile);
-    
-    static QString getSelectedText();
-
-    static QStringList getAllSpecialVariables(const QString &p_name, const QString &p_text);
-    
-    static QString replaceAllSepcialVariables(const QString &p_name,
-                                              const QString &p_text,
-                                              const QMap<QString, QString> &p_map);
-
-    static QString evaluateJsonExpr(const QJsonObject &p_obj,
-                                    const QString &p_expr);
-    
-    static QString getPathSeparator();
-    
-    static QString handleCommand(const QString &p_text,
-                                 QProcess *p_process,
-                                 const Task *p_task);
-    
-private:
-    TaskHelper();
-};
-
-} // ns vnotex
-
-#endif // TASKHELPER_H

+ 0 - 192
src/core/taskmgr.cpp

@@ -1,192 +0,0 @@
-#include "taskmgr.h"
-
-#include <QDir>
-#include <QDebug>
-#include <QFileSystemWatcher>
-#include <QJsonDocument>
-
-#include "configmgr.h"
-#include "coreconfig.h"
-#include "utils/pathutils.h"
-#include "utils/fileutils.h"
-#include "vnotex.h"
-#include "notebookmgr.h"
-#include "notebookconfigmgr/bundlenotebookconfigmgr.h"
-
-using namespace vnotex;
-
-QStringList TaskMgr::s_searchPaths;
-
-TaskMgr::TaskMgr(QObject *parent) 
-    : QObject(parent)
-{
-    m_watcher = new QFileSystemWatcher(this);
-}
-
-void TaskMgr::init()
-{   
-    // load all tasks and watch them
-    loadAllTask();
-    
-    connect(&VNoteX::getInst().getNotebookMgr(), &NotebookMgr::currentNotebookChanged,
-            this, &TaskMgr::loadAllTask);
-    connect(m_watcher, &QFileSystemWatcher::directoryChanged,
-            this, &TaskMgr::loadAllTask);
-    connect(m_watcher, &QFileSystemWatcher::fileChanged,
-            this, &TaskMgr::loadAllTask);
-}
-
-void TaskMgr::refresh()
-{
-    loadAvailableTasks();
-}
-
-QVector<Task*> TaskMgr::getAllTasks() const
-{
-    QVector<Task*> tasks;
-    tasks.append(m_appTasks);
-    tasks.append(m_userTasks);
-    tasks.append(m_notebookTasks);
-    return tasks;
-}
-
-const QVector<Task *> &TaskMgr::getAppTasks() const
-{
-    return m_appTasks;
-}
-
-const QVector<Task *> &TaskMgr::getUserTasks() const
-{
-    return m_userTasks;
-}
-
-const QVector<Task *> &TaskMgr::getNotebookTasks() const
-{
-    return m_notebookTasks;
-}
-
-void TaskMgr::addSearchPath(const QString &p_path)
-{
-    s_searchPaths << p_path;
-}
-
-QString TaskMgr::getNotebookTaskFolder()
-{
-    const auto &notebookMgr = VNoteX::getInst().getNotebookMgr();
-    auto id = notebookMgr.getCurrentNotebookId();
-    if (id == Notebook::InvalidId) return QString();
-    auto notebook = notebookMgr.findNotebookById(id);
-    if (!notebook) return QString();
-    auto configMgr = notebook->getConfigMgr();
-    if (!configMgr) return QString();
-    auto configMgrName = configMgr->getName();
-    if (configMgrName == "vx.vnotex") {
-        QDir dir(notebook->getRootFolderAbsolutePath());
-        dir.cd(BundleNotebookConfigMgr::getConfigFolderName());
-        if (!dir.cd("tasks")) return QString();
-        return dir.absolutePath();
-    } else {
-        qWarning() << "Unknow notebook config type"<< configMgrName <<"task will not be load.";
-    }
-    return QString();
-}
-
-void TaskMgr::addWatchPaths(const QStringList &list)
-{
-    if (list.isEmpty()) return ;
-    qDebug() << "addWatchPaths" << list;
-    m_watcher->addPaths(list);
-}
-
-void TaskMgr::clearWatchPaths()
-{
-    auto entrys = m_watcher->files();
-    if (!entrys.isEmpty()) {
-        m_watcher->removePaths(entrys);
-    }
-    entrys = m_watcher->directories();
-    if (!entrys.isEmpty()) {
-        m_watcher->removePaths(entrys);
-    }
-}
-
-void TaskMgr::clearTasks()
-{
-    m_appTasks.clear();
-    m_userTasks.clear();
-    m_notebookTasks.clear();
-    m_files.clear();
-}
-
-void TaskMgr::addAllTaskFolder()
-{
-    s_searchPaths.clear();
-    auto &configMgr = ConfigMgr::getInst();
-    // App scope task folder
-    addSearchPath(configMgr.getAppTaskFolder());
-    // User scope task folder
-    addSearchPath(configMgr.getUserTaskFolder());
-    // Notebook scope task folder
-    auto path = getNotebookTaskFolder();
-    if (!path.isNull()) addSearchPath(path);
-}
-
-void TaskMgr::loadAllTask()
-{
-    addAllTaskFolder();
-    loadAvailableTasks();
-    watchTaskEntrys();
-    emit taskChanged();
-}
-
-void TaskMgr::watchTaskEntrys()
-{
-    clearWatchPaths();
-    addWatchPaths(s_searchPaths);
-    for (const auto &pa : s_searchPaths) {
-        addWatchPaths(FileUtils::entryListRecursively(pa, QStringList(), QDir::AllDirs));
-    }
-    addWatchPaths(m_files);
-}
-
-void TaskMgr::loadAvailableTasks()
-{
-    m_files.clear();
-    auto &configMgr = ConfigMgr::getInst();
-    loadAvailableTasks(m_appTasks, configMgr.getAppTaskFolder());
-    loadAvailableTasks(m_userTasks, configMgr.getUserTaskFolder());
-    loadAvailableTasks(m_notebookTasks, getNotebookTaskFolder());
-}
-
-void TaskMgr::loadAvailableTasks(QVector<Task *> &p_tasks, const QString &p_searchPath)
-{
-    p_tasks.clear();
-    if (p_searchPath.isEmpty()) return ;
-    const auto taskFiles = FileUtils::entryListRecursively(p_searchPath, {"*.json"}, QDir::Files);
-    for (auto &file : taskFiles) {
-        m_files << file;
-        checkAndAddTaskFile(p_tasks, file);
-    }
-
-    {
-        QStringList list;
-        for (auto task : p_tasks) {
-            list << QFileInfo(task->getFile()).fileName();
-        }
-        if (!p_tasks.isEmpty()) qDebug() << "load tasks" << list;
-    }
-}
-
-void TaskMgr::checkAndAddTaskFile(QVector<Task *> &p_tasks, const QString &p_file)
-{
-    QJsonDocument json;
-    if (Task::isValidTaskFile(p_file, json)) {
-        const auto localeStr = ConfigMgr::getInst().getCoreConfig().getLocaleToUse();
-        p_tasks.push_back(Task::fromFile(p_file, json, localeStr, this));
-    }
-}
-
-void TaskMgr::deleteTask(Task *p_task)
-{
-    Q_UNUSED(p_task);
-}

+ 0 - 79
src/core/taskmgr.h

@@ -1,79 +0,0 @@
-#ifndef TASKMGR_H
-#define TASKMGR_H
-
-#include <QObject>
-
-#include <QVector>
-
-#include "task.h"
-
-class QFileSystemWatcher;
-
-namespace vnotex
-{
-    class TaskMgr : public QObject
-    {
-        Q_OBJECT
-    public:
-        
-        explicit TaskMgr(QObject *parent = nullptr);
-        
-        // It will be invoked after MainWindow show
-        void init();
-        
-        void refresh();
-        
-        QVector<Task*> getAllTasks() const;
-
-        const QVector<Task*> &getAppTasks() const;
-
-        const QVector<Task*> &getUserTasks() const;
-
-        const QVector<Task*> &getNotebookTasks() const;
-        
-        void deleteTask(Task *p_task);
-        
-        static void addSearchPath(const QString &p_path);
-        
-        static QString getNotebookTaskFolder();
-        
-    signals:
-        void taskChanged();
-        
-    private:
-        void addWatchPaths(const QStringList &list);
-        
-        void clearWatchPaths();
-
-        void clearTasks();
-
-        void addAllTaskFolder();
-        
-        void loadAllTask();
-        
-        void watchTaskEntrys();
-
-        void loadAvailableTasks();
-        
-        void loadAvailableTasks(QVector<Task*> &p_tasks, const QString &p_searchPath);
-        
-        void loadTasks(const QString &p_path);
-        
-        void checkAndAddTaskFile(QVector<Task*> &p_tasks, const QString &p_file);
-
-        QVector<Task*> m_appTasks;
-        QVector<Task*> m_userTasks;
-        QVector<Task*> m_notebookTasks;
-        
-        // all json files in task folder
-        // maybe invalid
-        QStringList m_files;
-        
-        QFileSystemWatcher *m_watcher;
-        
-        // List of path to search for tasks.
-        static QStringList s_searchPaths;
-    };
-} // ns vnotex
-
-#endif // TASKMGR_H

+ 0 - 334
src/core/taskvariablemgr.cpp

@@ -1,334 +0,0 @@
-#include "taskvariablemgr.h"
-
-#include <QRegularExpression>
-#include <QInputDialog>
-#include <QApplication>
-#include <QRandomGenerator>
-#include <QTimeZone>
-
-#include "vnotex.h"
-#include "task.h"
-#include "taskhelper.h"
-#include "shellexecution.h"
-#include "configmgr.h"
-#include "mainconfig.h"
-#include "notebook/notebook.h"
-#include <widgets/mainwindow.h>
-#include <widgets/dialogs/selectdialog.h>
-
-using namespace vnotex;
-
-
-TaskVariable::TaskVariable(TaskVariable::Type p_type, 
-                           const QString &p_name, 
-                           TaskVariable::Func p_func)
-    : m_type(p_type), m_name(p_name), m_func(p_func)
-{
-    
-}
-
-
-TaskVariableMgr::TaskVariableMgr()
-    : m_initialized(false)
-{
-    
-}
-
-void TaskVariableMgr::refresh()
-{
-    init();
-}
-
-QString TaskVariableMgr::evaluate(const QString &p_text, 
-                                   const Task *p_task) const
-{
-    auto text = p_text;
-    auto eval = [&text](const QString &p_name, std::function<QString()> p_func) {
-        auto reg = QRegularExpression(QString(R"(\$\{[\t ]*%1[\t ]*\})").arg(p_name));
-        if (text.contains(reg)) {
-            text.replace(reg, p_func());
-        }
-    };
-    
-    // current notebook variables
-    {
-        eval("notebookFolder", []() { 
-            auto notebook = TaskHelper::getCurrentNotebook();
-            if (notebook) {
-                return TaskHelper::normalPath(notebook->getRootFolderAbsolutePath()); 
-            } else return QString();
-        });
-        eval("notebookFolderBasename", []() { 
-            auto notebook = TaskHelper::getCurrentNotebook();
-            if (notebook) {
-                auto folder = notebook->getRootFolderAbsolutePath();
-                return QDir(folder).dirName(); 
-            } else return QString();
-        });
-        eval("notebookName", []() { 
-            auto notebook = TaskHelper::getCurrentNotebook();
-            if (notebook) {
-                return notebook->getName(); 
-            } else return QString();
-        });
-        eval("notebookName", []() { 
-            auto notebook = TaskHelper::getCurrentNotebook();
-            if (notebook) {
-                return notebook->getDescription(); 
-            } else return QString();
-        });
-    }
-    
-    // current file variables
-    {
-        eval("file", []() { 
-            return TaskHelper::normalPath(TaskHelper::getCurrentFile()); 
-        });
-        eval("fileNotebookFolder", []() {
-            auto file = TaskHelper::getCurrentFile();
-            return TaskHelper::normalPath(TaskHelper::getFileNotebookFolder(file)); 
-        });
-        eval("relativeFile", []() {
-            auto file = TaskHelper::getCurrentFile();
-            auto folder = TaskHelper::getFileNotebookFolder(file);
-            return QDir(folder).relativeFilePath(file); 
-        });
-        eval("fileBasename", []() {
-            auto file = TaskHelper::getCurrentFile();
-            return QFileInfo(file).fileName();
-        });
-        eval("fileBasename", []() {
-            auto file = TaskHelper::getCurrentFile();
-            return QFileInfo(file).completeBaseName();
-        });
-        eval("fileDirname", []() {
-            auto file = TaskHelper::getCurrentFile();
-            return TaskHelper::normalPath(QFileInfo(file).dir().absolutePath());
-        });
-        eval("fileExtname", []() {
-            auto file = TaskHelper::getCurrentFile();
-            return QFileInfo(file).suffix();
-        });
-        eval("selectedText", []() {
-            return TaskHelper::getSelectedText();
-        });
-        eval("cwd", [p_task]() { 
-            return TaskHelper::normalPath(p_task->getOptionsCwd()); 
-        });
-        eval("taskFile", [p_task]() { 
-            return TaskHelper::normalPath(p_task->getFile()); 
-        });
-        eval("taskDirname", [p_task]() { 
-            return TaskHelper::normalPath(QFileInfo(p_task->getFile()).dir().absolutePath());
-        });
-        eval("execPath", []() { 
-            return TaskHelper::normalPath(qApp->applicationFilePath()); 
-        });
-        eval("pathSeparator", []() { 
-            return TaskHelper::getPathSeparator(); 
-        });
-        eval("notebookTaskFolder", []() { 
-            return TaskHelper::normalPath(TaskMgr::getNotebookTaskFolder());
-        });
-        eval("userTaskFolder", []() { 
-            return TaskHelper::normalPath(ConfigMgr::getInst().getUserTaskFolder());
-        });
-        eval("appTaskFolder", []() { 
-            return TaskHelper::normalPath(ConfigMgr::getInst().getAppTaskFolder());
-        });
-        eval("userThemeFolder", []() { 
-            return TaskHelper::normalPath(ConfigMgr::getInst().getUserThemeFolder());
-        });
-        eval("appThemeFolder", []() { 
-            return TaskHelper::normalPath(ConfigMgr::getInst().getAppThemeFolder());
-        });
-        eval("userDocsFolder", []() { 
-            return TaskHelper::normalPath(ConfigMgr::getInst().getUserDocsFolder());
-        });
-        eval("appDocsFolder", []() { 
-            return TaskHelper::normalPath(ConfigMgr::getInst().getAppDocsFolder());
-        });
-    }
-    
-    // Magic variables
-    {
-        auto cDT = QDateTime::currentDateTime();
-        for(auto s : {
-            "d", "dd", "ddd", "dddd", "M", "MM", "MMM", "MMMM", 
-            "yy", "yyyy", "h", "hh", "H", "HH", "m", "mm", 
-            "s", "ss", "z", "zzz", "AP", "A", "ap", "a"
-        }) eval(QString("magic:%1").arg(s), [s]() {
-            return QDateTime::currentDateTime().toString(s);
-        }); 
-        eval("magic:random", []() {
-            return QString::number(QRandomGenerator::global()->generate());
-        });
-        eval("magic:random_d", []() {
-            return QString::number(QRandomGenerator::global()->generate());
-        });
-        eval("magic:date", []() {
-            return QDate::currentDate().toString("yyyy-MM-dd");
-        });
-        eval("magic:da", []() {
-            return QDate::currentDate().toString("yyyyMMdd");
-        });
-        eval("magic:time", []() {
-            return QTime::currentTime().toString("hh:mm:ss");
-        });
-        eval("magic:datetime", []() {
-            return QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
-        });
-        eval("magic:dt", []() {
-            return QDateTime::currentDateTime().toString("yyyyMMdd hh:mm:ss");
-        });
-        eval("magic:note", []() {
-            auto file = TaskHelper::getCurrentFile();
-            return QFileInfo(file).fileName();
-        });
-        eval("magic:no", []() {
-            auto file = TaskHelper::getCurrentFile();
-            return QFileInfo(file).completeBaseName();
-        });
-        eval("magic:t", []() {
-            auto dt = QDateTime::currentDateTime();
-            return dt.timeZone().displayName(dt, QTimeZone::ShortName);
-        });
-        eval("magic:w", []() {
-            return QString::number(QDate::currentDate().weekNumber());
-        });
-        eval("magic:att", []() {
-            // TODO
-            return QString();
-        });
-    }
-    
-    // environment variables
-    do {
-        QMap<QString, QString> map;
-        auto list = TaskHelper::getAllSpecialVariables("env", p_text);
-        list.erase(std::unique(list.begin(), list.end()), list.end());
-        if (list.isEmpty()) break;
-        for (const auto &name : list) {
-            auto value = QProcessEnvironment::systemEnvironment().value(name);
-            map.insert(name, value);
-        }
-        text = TaskHelper::replaceAllSepcialVariables("env", text, map);
-    } while(0);
-    
-    // config variables
-    do {
-        const auto config_obj = ConfigMgr::getInst().getConfig().toJson();
-        QMap<QString, QString> map;
-        auto list = TaskHelper::getAllSpecialVariables("config", p_text);
-        if (list.isEmpty()) break;
-        list.erase(std::unique(list.begin(), list.end()), list.end());
-        for (const auto &name : list) {
-            auto value = TaskHelper::evaluateJsonExpr(config_obj, name);
-            qDebug() << "insert" << name << value;
-            map.insert(name, value);
-        }
-        text = TaskHelper::replaceAllSepcialVariables("config", text, map);
-    } while(0);
-    
-    // input variables
-    text = evaluateInputVariables(text, p_task);
-    // shell variables
-    text = evaluateShellVariables(text, p_task);
-    return text;
-}
-
-QStringList TaskVariableMgr::evaluate(const QStringList &p_list, 
-                                      const Task *p_task) const
-{
-    QStringList list;
-    for (const auto &s : p_list) {
-        list << evaluate(s, p_task);
-    }
-    return list;
-}
-
-void TaskVariableMgr::init()
-{
-    if (m_initialized) return ;
-    m_initialized = true;
-    
-    add(TaskVariable::FunctionBased,
-        "file",
-        [](const TaskVariable *, const Task *) {
-         return QString();
-    });
-}
-
-void TaskVariableMgr::add(TaskVariable::Type p_type, 
-                          const QString &p_name, 
-                          TaskVariable::Func p_func)
-{
-    m_predefs.insert(p_name, TaskVariable(p_type, p_name, p_func));
-}
-
-QString TaskVariableMgr::evaluateInputVariables(const QString &p_text, 
-                                                const Task *p_task) const
-{
-    QMap<QString, QString> map;
-    auto list = TaskHelper::getAllSpecialVariables("input", p_text);
-    list.erase(std::unique(list.begin(), list.end()), list.end());
-    if (list.isEmpty()) return p_text;
-    for (const auto &id : list) {
-        auto input = p_task->getInput(id);
-        QString text;
-        auto mainwin = VNoteX::getInst().getMainWindow();
-        if (input.type == "promptString") {
-            auto desc = evaluate(input.description, p_task);
-            auto defaultText = evaluate(input.default_, p_task);
-            QInputDialog dialog(mainwin);
-            dialog.setInputMode(QInputDialog::TextInput);
-            if (input.password) dialog.setTextEchoMode(QLineEdit::Password);
-            else dialog.setTextEchoMode(QLineEdit::Normal);
-            dialog.setWindowTitle(p_task->getLabel());
-            dialog.setLabelText(desc);
-            dialog.setTextValue(defaultText);
-            if (dialog.exec() == QDialog::Accepted) {
-                text = dialog.textValue();
-            } else {
-                throw "TaskCancle";
-            }
-        } else if (input.type == "pickString") {
-            // TODO: select description
-            SelectDialog dialog(p_task->getLabel(), input.description, mainwin);
-            for (int i = 0; i < input.options.size(); i++) {
-                dialog.addSelection(input.options.at(i), i);
-            }
-    
-            if (dialog.exec() == QDialog::Accepted) {
-                int selection = dialog.getSelection();
-                text = input.options.at(selection);
-            } else {
-                throw "TaskCancle";
-            }
-        }
-        map.insert(input.id, text);
-    }
-    return TaskHelper::replaceAllSepcialVariables("input", p_text, map);
-}
-
-QString TaskVariableMgr::evaluateShellVariables(const QString &p_text, 
-                                                const Task *p_task) const
-{
-    QMap<QString, QString> map;
-    auto list = TaskHelper::getAllSpecialVariables("shell", p_text);
-    list.erase(std::unique(list.begin(), list.end()), list.end());
-    if (list.isEmpty()) return p_text;
-    for (const auto &cmd : list) {
-        QProcess process;
-        process.setWorkingDirectory(p_task->getOptionsCwd());
-        ShellExecution::setupProcess(&process, cmd);
-        process.start();
-        if (!process.waitForStarted(1000) || !process.waitForFinished(1000)) {
-            throw "Shell variable execution timeout";
-        }
-        auto res = process.readAllStandardOutput().trimmed();
-        map.insert(cmd, res);
-    }
-    return TaskHelper::replaceAllSepcialVariables("shell", p_text, map);
-}
-

+ 0 - 73
src/core/taskvariablemgr.h

@@ -1,73 +0,0 @@
-#ifndef TASKVARIABLEMGR_H
-#define TASKVARIABLEMGR_H
-
-#include <functional>
-
-#include <QHash>
-#include <QString>
-#include <QSharedPointer>
-
-namespace vnotex {
-
-class Task;
-class TaskVariable;
-class TaskVariableMgr;
-class Notebook;
-
-class TaskVariable {
-public:
-    enum Type
-    {
-        // Definition is plain text.
-        Simple = 0,
-    
-        // Definition is a function call to get the value.
-        FunctionBased,
-    
-        // Like FunctionBased, but should re-evaluate the value for each occurence.
-        Dynamic,
-        
-        Invalid
-    };
-    typedef std::function<QString(const TaskVariable *,
-                                  const Task*)> Func;
-    TaskVariable(Type p_type,
-                 const QString &p_name,
-                 Func p_func = nullptr);
-private:
-    Type m_type;
-    QString m_name;
-    Func m_func;
-};
-
-class TaskVariableMgr
-{
-public:
-    TaskVariableMgr();
-    void refresh();
-    QString evaluate(const QString &p_text, 
-                     const Task *p_task) const;
-    
-    QStringList evaluate(const QStringList &p_list,
-                         const Task *p_task) const;
-    
-private:
-    void init();
-    
-    void add(TaskVariable::Type p_type,
-             const QString &p_name,
-             TaskVariable::Func p_func = nullptr);    
-    
-    QString evaluateInputVariables(const QString &p_text, 
-                                   const Task *p_task) const;
-    
-    QString evaluateShellVariables(const QString &p_text,
-                                   const Task *p_task) const;
-    
-    QHash<QString, TaskVariable> m_predefs;
-    bool m_initialized;
-};
-
-} // ns vnotex
-
-#endif // TASKVARIABLEMGR_H

+ 3 - 0
src/core/vnotex.cpp

@@ -14,6 +14,7 @@
 #include "quickaccesshelper.h"
 
 #include <utils/docsutils.h>
+#include <task/taskmgr.h>
 
 
 using namespace vnotex;
@@ -63,6 +64,8 @@ void VNoteX::initTaskMgr()
 {
     Q_ASSERT(!m_taskMgr);
     m_taskMgr = new TaskMgr(this);
+    connect(m_taskMgr, &TaskMgr::taskOutputRequested,
+            this, &VNoteX::showOutputRequested);
 }
 
 ThemeMgr &VNoteX::getThemeMgr() const

+ 1 - 1
src/core/vnotex.h

@@ -6,7 +6,6 @@
 
 #include "noncopyable.h"
 #include "thememgr.h"
-#include "taskmgr.h"
 #include "global.h"
 
 namespace vnotex
@@ -19,6 +18,7 @@ namespace vnotex
     class Event;
     class Notebook;
     struct ComplexLocation;
+    class TaskMgr;
 
     class VNoteX : public QObject, private Noncopyable
     {

File diff suppressed because it is too large
+ 0 - 1
src/data/core/icons/task_menu.svg


+ 3 - 3
src/data/extra/tasks/git/git.json

@@ -37,12 +37,12 @@
                     "id": "msg",
                     "type": "promptString",
                     "description": {
-                        "en_US": "Please provide a commit message",
+                        "en_US": "Please input the commit message",
                         "zh_CN": "请输入提交信息",
                         "ja_JP": "コミットメッセージを提供してください"
                     },
                     "default": {
-                        "en_US": "Update note on ${magic:datetime}",
+                        "en_US": "Update note at ${magic:datetime}",
                         "zh_CN": "更新笔记于 ${magic:datetime}",
                         "ja_JP": "アップデート ${magic:datetime}"
                     }
@@ -77,4 +77,4 @@
             "command": "git log -10 --graph --pretty=format:'%h -%d %s (%cr) <%an>' --abbrev-commit"
         }
     ]
-}
+}

+ 3 - 1
src/export/webviewexporter.cpp

@@ -192,13 +192,15 @@ bool WebViewExporter::writeHtmlFile(const QString &p_file,
                                     bool p_embedImages)
 {
     const auto baseName = QFileInfo(p_file).completeBaseName();
-    auto title = QString("%1 - %2").arg(baseName, ConfigMgr::c_appName);
+
     const QString resourceFolderName = baseName + "_files";
     auto resourceFolder = PathUtils::concatenateFilePath(PathUtils::parentDirPath(p_file), resourceFolderName);
 
     qDebug() << "HTML files folder" << resourceFolder;
 
     auto htmlContent = m_exportHtmlTemplate;
+
+    const auto title = QString("%1").arg(baseName);
     HtmlTemplateHelper::fillTitle(htmlContent, title);
 
     if (!p_styleContent.isEmpty() && p_embedStyles) {

+ 24 - 7
src/snippet/snippetmgr.cpp

@@ -20,7 +20,9 @@
 
 using namespace vnotex;
 
-const QString SnippetMgr::c_snippetSymbolRegExp = QString("%([^%]+)%");
+const QChar SnippetMgr::c_snippetSymbolGuard = QLatin1Char('%');
+
+const QString SnippetMgr::c_snippetSymbolRegExp = QString("%1([^%]+)%1").arg(c_snippetSymbolGuard);
 
 SnippetMgr::SnippetMgr()
 {
@@ -231,10 +233,11 @@ void SnippetMgr::applySnippet(const QString &p_name,
     p_textEdit->setTextCursor(cursor);
 }
 
-QString SnippetMgr::applySnippetBySymbol(const QString &p_content) const
+QString SnippetMgr::applySnippetBySymbol(const QString &p_content,
+                                         const OverrideMap &p_overrides) const
 {
     int offset = 0;
-    return applySnippetBySymbol(p_content, QString(), offset);
+    return applySnippetBySymbol(p_content, QString(), offset, p_overrides);
 }
 
 QString SnippetMgr::applySnippetBySymbol(const QString &p_content,
@@ -244,11 +247,11 @@ QString SnippetMgr::applySnippetBySymbol(const QString &p_content,
 {
     QString content(p_content);
 
-    int maxTimes = 100;
+    int maxTimesAtSamePos = 100;
 
     QRegularExpression regExp(c_snippetSymbolRegExp);
     int pos = 0;
-    while (pos < content.size() && maxTimes-- > 0) {
+    while (pos < content.size()) {
         QRegularExpressionMatch match;
         int idx = content.indexOf(regExp, pos, &match);
         if (idx == -1) {
@@ -288,6 +291,13 @@ QString SnippetMgr::applySnippetBySymbol(const QString &p_content,
         }
 
         // @afterText may still contains snippet symbol.
+        if (pos == idx) {
+            if (--maxTimesAtSamePos == 0) {
+                break;
+            }
+        } else {
+            maxTimesAtSamePos = 100;
+        }
         pos = idx;
     }
 
@@ -426,8 +436,10 @@ void SnippetMgr::addDynamicSnippet(QVector<QSharedPointer<Snippet>> &p_snippets,
 SnippetMgr::OverrideMap SnippetMgr::generateOverrides(const Buffer *p_buffer)
 {
     OverrideMap overrides;
-    overrides.insert(QStringLiteral("note"), p_buffer->getName());
-    overrides.insert(QStringLiteral("no"), QFileInfo(p_buffer->getName()).completeBaseName());
+    if (p_buffer) {
+        overrides.insert(QStringLiteral("note"), p_buffer->getName());
+        overrides.insert(QStringLiteral("no"), QFileInfo(p_buffer->getName()).completeBaseName());
+    }
     return overrides;
 }
 
@@ -438,3 +450,8 @@ SnippetMgr::OverrideMap SnippetMgr::generateOverrides(const QString &p_fileName)
     overrides.insert(QStringLiteral("no"), QFileInfo(p_fileName).completeBaseName());
     return overrides;
 }
+
+QString SnippetMgr::generateSnippetSymbol(const QString &p_snippetName)
+{
+    return c_snippetSymbolGuard + p_snippetName + c_snippetSymbolGuard;
+}

+ 7 - 2
src/snippet/snippetmgr.h

@@ -60,14 +60,19 @@ namespace vnotex
                                      int &p_cursorOffset,
                                      const OverrideMap &p_overrides = OverrideMap()) const;
 
-        QString applySnippetBySymbol(const QString &p_content) const;
+        QString applySnippetBySymbol(const QString &p_content,
+                                     const OverrideMap &p_overrides = OverrideMap()) const;
 
         // Generate standard overrides for given buffer.
         static OverrideMap generateOverrides(const Buffer *p_buffer);
 
-        // Generate standard overrides.
+        // Generate standard overrides for given file name.
         static OverrideMap generateOverrides(const QString &p_fileName);
 
+        static QString generateSnippetSymbol(const QString &p_snippetName);
+
+        static const QChar c_snippetSymbolGuard;
+
         // %name%.
         // Captured texts:
         // 1 - The name of the snippet.

+ 2 - 0
src/src.pro

@@ -56,6 +56,8 @@ include($$PWD/snippet/snippet.pri)
 
 include($$PWD/imagehost/imagehost.pri)
 
+include($$PWD/task/task.pri)
+
 include($$PWD/core/core.pri)
 
 include($$PWD/widgets/widgets.pri)

+ 72 - 0
src/task/shellexecution.cpp

@@ -0,0 +1,72 @@
+#include "shellexecution.h"
+
+#include <QFileInfo>
+#include <QProcess>
+
+using namespace vnotex;
+
+void ShellExecution::setupProcess(QProcess *p_process,
+                                  const QString &p_program,
+                                  const QStringList &p_args,
+                                  const QString &p_shellExec,
+                                  const QStringList &p_shellArgs)
+{
+    auto shellExec = p_shellExec.isNull() ? defaultShell() : p_shellExec;
+    auto shellArgs = p_shellArgs.isEmpty() ? defaultShellArguments(shellExec) : p_shellArgs;
+
+    p_process->setProgram(shellExec);
+
+    const auto shell = shellBasename(p_shellExec);
+    QStringList allArgs(shellArgs);
+    if (shell == "bash") {
+        allArgs << (QStringList() << p_program << quoteSpaces(p_args)).join(' ');
+    } else {
+        allArgs << p_program << p_args;
+    }
+    p_process->setArguments(allArgs);
+}
+
+QString ShellExecution::shellBasename(const QString &p_shell)
+{
+    return QFileInfo(p_shell).baseName().toLower();
+}
+
+QString ShellExecution::defaultShell()
+{
+#ifdef Q_OS_WIN
+    return QStringLiteral("PowerShell.exe");
+#else
+    return QStringLiteral("/bin/bash");
+#endif
+}
+
+QStringList ShellExecution::defaultShellArguments(const QString &p_shell)
+{
+    auto shell = shellBasename(p_shell);
+    if (shell == "cmd") {
+        return {"/C"};
+    } else if (shell == "powershell" || p_shell == "pwsh") {
+        return {"-Command"};
+    } else if (shell == "bash") {
+        return {"-c"};
+    }
+    return {};
+}
+
+QString ShellExecution::quoteSpace(const QString &p_arg)
+{
+    if (p_arg.contains(QLatin1Char(' '))) {
+        return QLatin1Char('"') + p_arg + QLatin1Char('"');
+    } else {
+        return p_arg;
+    }
+}
+
+QStringList ShellExecution::quoteSpaces(const QStringList &p_args)
+{
+    QStringList args;
+    for (const auto &arg : p_args) {
+        args << quoteSpace(arg);
+    }
+    return args;
+}

+ 34 - 0
src/task/shellexecution.h

@@ -0,0 +1,34 @@
+#ifndef SHELLEXECUTION_H
+#define SHELLEXECUTION_H
+
+#include <QStringList>
+
+class QProcess;
+
+namespace vnotex
+{
+    class ShellExecution
+    {
+    public:
+        ShellExecution() = delete;
+
+        static void setupProcess(QProcess *p_process,
+                                 const QString &p_program,
+                                 const QStringList &p_args = QStringList(),
+                                 const QString &p_shellExec = QString(),
+                                 const QStringList &p_shellArgs = QStringList());
+
+        static QString defaultShell();
+
+        static QStringList defaultShellArguments(const QString &p_shell);
+
+    private:
+        static QString shellBasename(const QString &p_shell);
+
+        static QString quoteSpace(const QString &p_arg);
+
+        static QStringList quoteSpaces(const QStringList &p_args);
+    };
+}
+
+#endif // SHELLEXECUTION_H

+ 588 - 0
src/task/task.cpp

@@ -0,0 +1,588 @@
+#include "task.h"
+
+#include <QJsonDocument>
+#include <QVersionNumber>
+#include <QDebug>
+#include <QJsonValue>
+#include <QJsonArray>
+#include <QAction>
+#include <QRegularExpression>
+#include <QInputDialog>
+#include <QTextCodec>
+#include <QRandomGenerator>
+#include <QJsonObject>
+#include <QProcess>
+#include <QScopedPointer>
+
+#include <utils/fileutils.h>
+#include <utils/pathutils.h>
+#include <core/vnotex.h>
+#include <core/exception.h>
+#include <notebook/notebook.h>
+#include <buffer/buffer.h>
+
+#include "shellexecution.h"
+#include "taskmgr.h"
+
+using namespace vnotex;
+
+QString Task::s_latestVersion = "0.1.3";
+
+QSharedPointer<Task> Task::fromFile(const QString &p_file, const QString &p_locale, TaskMgr *p_taskMgr)
+{
+    QSharedPointer<Task> task(new Task(p_locale, p_file, p_taskMgr, nullptr));
+    const auto obj = FileUtils::readJsonFile(p_file);
+    if (fromJson(task.data(), obj)) {
+        return task;
+    }
+    return nullptr;
+}
+
+bool Task::fromJson(Task *p_task, const QJsonObject &p_obj)
+{
+    // For child task, it will inherit the version from parent.
+    if (p_obj.contains("version")) {
+        p_task->m_dto.version = p_obj["version"].toString();
+    }
+
+    const auto version = QVersionNumber::fromString(p_task->getVersion());
+    if (version.isNull()) {
+        qWarning() << "invalid task" << p_task->m_dto._source;
+        return false;
+    }
+
+    if (version < QVersionNumber(1, 0, 0)) {
+        return fromJsonV0(p_task, p_obj);
+    } else {
+        qWarning() << "unknown task version" << version << p_task->m_dto._source;
+        return false;
+    }
+}
+
+bool Task::fromJsonV0(Task *p_task, const QJsonObject &p_obj, bool p_mergeTasks)
+{
+    if (p_obj.contains("type")) {
+        p_task->m_dto.type = p_obj["type"].toString();
+    }
+
+    if (p_obj.contains("icon")) {
+        QString iconPath = p_obj["icon"].toString();
+        if (!iconPath.isEmpty()) {
+            if (QDir::isRelativePath(iconPath)) {
+                iconPath = QFileInfo(p_task->m_dto._source).dir().absoluteFilePath(iconPath);
+            }
+
+            if (QFileInfo::exists(iconPath)) {
+                p_task->m_dto.icon = iconPath;
+            } else {
+                qWarning() << "task icon does not exist" << p_task->getLabel() << iconPath;
+            }
+        }
+    }
+
+    if (p_obj.contains("shortcut")) {
+        p_task->m_dto.shortcut = p_obj["shortcut"].toString();
+    }
+
+    if (p_obj.contains("type")) {
+        p_task->m_dto.type = p_obj["type"].toString();
+    }
+
+    if (p_obj.contains("command")) {
+        p_task->m_dto.command = getLocaleString(p_obj["command"], p_task->m_locale);
+    }
+
+    if (p_obj.contains("args")) {
+        p_task->m_dto.args = getLocaleStringList(p_obj["args"], p_task->m_locale);
+    }
+
+    if (p_obj.contains("label")) {
+        p_task->m_dto.label = getLocaleString(p_obj["label"], p_task->m_locale);
+    } else if (p_task->m_dto.label.isNull() && !p_task->m_dto.command.isNull()) {
+        p_task->m_dto.label = p_task->m_dto.command;
+    }
+
+    if (p_obj.contains("options")) {
+        auto options = p_obj["options"].toObject();
+
+        if (options.contains("cwd")) {
+            p_task->m_dto.options.cwd = options["cwd"].toString();
+        }
+
+        if (options.contains("env")) {
+            p_task->m_dto.options.env.clear();
+            auto env = options["env"].toObject();
+            for (auto it = env.begin(); it != env.end(); it++) {
+                auto value = getLocaleString(it.value(), p_task->m_locale);
+                p_task->m_dto.options.env.insert(it.key(), value);
+            }
+        }
+
+        if (options.contains("shell") && p_task->getType() == "shell") {
+            auto shell = options["shell"].toObject();
+
+            if (shell.contains("executable")) {
+                p_task->m_dto.options.shell.executable = shell["executable"].toString();
+            }
+
+            if (shell.contains("args")) {
+                p_task->m_dto.options.shell.args.clear();
+
+                const auto arr = shell["args"].toArray();
+                for (int i = 0; i < arr.size(); ++i) {
+                    p_task->m_dto.options.shell.args << arr[i].toString();
+                }
+            }
+        }
+    }
+
+    if (p_obj.contains("tasks")) {
+        if (!p_mergeTasks) {
+            p_task->m_children.clear();
+        }
+
+        auto arr = p_obj["tasks"].toArray();
+        for (int i = 0; i < arr.size(); ++i) {
+            QScopedPointer<Task> childTask(new Task(p_task->m_locale, p_task->getFile(), p_task->m_taskMgr, p_task));
+            if (fromJson(childTask.data(), arr[i].toObject())) {
+                connect(childTask.data(), &Task::outputRequested,
+                        p_task, &Task::outputRequested);
+                p_task->m_children.append(childTask.take());
+            }
+        }
+    }
+
+    if (p_obj.contains("inputs")) {
+        p_task->m_dto.inputs.clear();
+        auto arr = p_obj["inputs"].toArray();
+        for (int i = 0; i < arr.size(); ++i) {
+            const auto inputObj = arr[i].toObject();
+            InputDTO input;
+            if (inputObj.contains("id")) {
+                input.id = inputObj["id"].toString();
+            } else {
+                qWarning() << "Input configuration not contains id";
+            }
+
+            if (inputObj.contains("type")) {
+                input.type = inputObj["type"].toString();
+            } else {
+                input.type = "promptString";
+            }
+
+            if (inputObj.contains("description")) {
+                input.description = getLocaleString(inputObj["description"], p_task->m_locale);
+            }
+
+            if (inputObj.contains("default")) {
+                input.default_ = getLocaleString(inputObj["default"], p_task->m_locale);
+            }
+
+            if (input.type == "promptString" && inputObj.contains("password")) {
+                input.password = inputObj["password"].toBool();
+            } else {
+                input.password = false;
+            }
+
+            if (input.type == "pickString") {
+                if (inputObj.contains("options")) {
+                    input.options = getLocaleStringList(inputObj["options"], p_task->m_locale);
+                }
+
+                if (!input.default_.isNull() && !input.options.contains(input.default_)) {
+                    qWarning() << "default of input must be one of the option values";
+                }
+            }
+
+            p_task->m_dto.inputs << input;
+        }
+    }
+
+    if (p_obj.contains("messages")) {
+        p_task->m_dto.messages.clear();
+        auto arr = p_obj["messages"].toArray();
+        for (int i = 0; i < arr.size(); ++i) {
+            const auto msgObj = arr[i].toObject();
+            MessageDTO msg;
+            if (msgObj.contains("id")) {
+                msg.id = msgObj["id"].toString();
+            } else {
+                qWarning() << "Message configuration not contain id";
+            }
+
+            if (msgObj.contains("type")) {
+                msg.type = msgObj["type"].toString();
+            } else {
+                msg.type = "information";
+            }
+
+            if (msgObj.contains("title")) {
+                msg.title = getLocaleString(msgObj["title"], p_task->m_locale);
+            }
+
+            if (msgObj.contains("text")) {
+                msg.text = getLocaleString(msgObj["text"], p_task->m_locale);
+            }
+
+            if (msgObj.contains("detailedText")) {
+                msg.detailedText = getLocaleString(msgObj["detailedText"], p_task->m_locale);
+            }
+
+            if (msgObj.contains("buttons")) {
+                auto buttonsArr = msgObj["buttons"].toArray();
+                for (int j = 0; j < buttonsArr.size(); ++j) {
+                    const auto btnObj = buttonsArr[j].toObject();
+                    ButtonDTO btn;
+                    btn.text = getLocaleString(btnObj["text"], p_task->m_locale);
+                    msg.buttons << btn;
+                }
+            }
+
+            p_task->m_dto.messages << msg;
+        }
+    }
+
+    // OS-specific task configuration
+#if defined (Q_OS_WIN)
+    #define OS_SPEC "windows"
+#elif defined (Q_OS_MACOS)
+    #define OS_SPEC "osx"
+#else
+    #define OS_SPEC "linux"
+#endif
+
+    if (p_obj.contains(OS_SPEC)) {
+        const auto osObj = p_obj[OS_SPEC].toObject();
+        fromJsonV0(p_task, osObj, true);
+    }
+
+#undef OS_SPEC
+
+    return true;
+}
+
+const QString &Task::getVersion() const
+{
+    return m_dto.version;
+}
+
+const QString &Task::getType() const
+{
+    return m_dto.type;
+}
+
+QString Task::getCommand()
+{
+    return variableMgr().evaluate(this, m_dto.command);
+}
+
+QStringList Task::getArgs()
+{
+    return variableMgr().evaluate(this, m_dto.args);
+}
+
+const QString &Task::getLabel() const
+{
+    return m_dto.label;
+}
+
+const QString &Task::getIcon() const
+{
+    return m_dto.icon;
+}
+
+const QString &Task::getShortcut() const
+{
+    return m_dto.shortcut;
+}
+
+QString Task::getOptionsCwd()
+{
+    auto cwd = m_dto.options.cwd;
+    if (!cwd.isNull()) {
+        return variableMgr().evaluate(this, cwd);
+    }
+
+    auto notebook = TaskVariableMgr::getCurrentNotebook();
+    if (notebook) {
+        cwd = notebook->getRootFolderAbsolutePath();
+    }
+
+    if (!cwd.isNull()) {
+        return cwd;
+    }
+
+    auto buffer = TaskVariableMgr::getCurrentBuffer();
+    if (buffer) {
+        return QFileInfo(buffer->getPath()).dir().absolutePath();
+    }
+
+    return QFileInfo(m_dto._source).dir().absolutePath();
+}
+
+const QMap<QString, QString> &Task::getOptionsEnv() const
+{
+    return m_dto.options.env;
+}
+
+const QString &Task::getOptionsShellExecutable() const
+{
+    return m_dto.options.shell.executable;
+}
+
+QStringList Task::getOptionsShellArgs()
+{
+    if (m_dto.options.shell.args.isEmpty()) {
+        return ShellExecution::defaultShellArguments(m_dto.options.shell.executable);
+    } else {
+        return variableMgr().evaluate(this, m_dto.options.shell.args);
+    }
+}
+
+const QVector<Task *> &Task::getChildren() const
+{
+    return m_children;
+}
+
+const QVector<InputDTO> &Task::getInputs() const
+{
+    return m_dto.inputs;
+}
+
+const InputDTO *Task::findInput(const QString &p_id) const
+{
+    for (const auto &input : m_dto.inputs) {
+        if (input.id == p_id) {
+            return &input;
+        }
+    }
+
+    qWarning() << "input" << p_id << "not found for task" << getLabel();
+    return nullptr;
+}
+
+const MessageDTO *Task::findMessage(const QString &p_id) const
+{
+    for (const auto &msg : m_dto.messages) {
+        if (msg.id == p_id) {
+            return &msg;
+        }
+    }
+
+    qWarning() << "message" << p_id << "not found for task" << getLabel();
+    return nullptr;
+}
+
+const QString &Task::getFile() const
+{
+    return m_dto._source;
+}
+
+Task::Task(const QString &p_locale,
+           const QString &p_file,
+           TaskMgr *p_taskMgr,
+           QObject *p_parent)
+    : QObject(p_parent),
+      m_taskMgr(p_taskMgr),
+      m_locale(p_locale)
+{
+    m_dto._source = p_file;
+    m_dto.version = s_latestVersion;
+    m_dto.type = "shell";
+    m_dto.options.shell.executable = ShellExecution::defaultShell();
+
+    // Inherit configuration.
+    m_parent = qobject_cast<Task *>(p_parent);
+    if (m_parent) {
+        m_dto.version = m_parent->m_dto.version;
+        m_dto.type = m_parent->m_dto.type;
+        m_dto.command = m_parent->m_dto.command;
+        m_dto.args = m_parent->m_dto.args;
+        m_dto.options.cwd = m_parent->m_dto.options.cwd;
+        m_dto.options.env = m_parent->m_dto.options.env;
+        m_dto.options.shell.executable = m_parent->m_dto.options.shell.executable;
+        m_dto.options.shell.args = m_parent->m_dto.options.shell.args;
+        // Do not inherit label/inputs/tasks.
+    } else {
+        m_dto.label = QFileInfo(p_file).baseName();
+    }
+}
+
+QProcess *Task::setupProcess()
+{
+    setCancelled(false);
+
+    auto command = getCommand();
+    if (command.isEmpty()) {
+        return nullptr;
+    }
+
+    QScopedPointer<QProcess> scopedProcess(new QProcess(this));
+
+    auto process = scopedProcess.data();
+    process->setWorkingDirectory(getOptionsCwd());
+
+    const auto &optionsEnv = getOptionsEnv();
+    if (!optionsEnv.isEmpty()) {
+        auto env = QProcessEnvironment::systemEnvironment();
+        for (auto it = optionsEnv.begin(); it != optionsEnv.end(); it++) {
+            env.insert(it.key(), it.value());
+        }
+        process->setProcessEnvironment(env);
+    }
+
+    const auto args = getArgs();
+    const auto &type = getType();
+
+    if (type == "shell") {
+        ShellExecution::setupProcess(process,
+                                     command,
+                                     args,
+                                     getOptionsShellExecutable(),
+                                     getOptionsShellArgs());
+    } else if (getType() == "process") {
+        process->setProgram(command);
+        process->setArguments(args);
+    }
+
+    if (isCancelled()) {
+        return nullptr;
+    }
+
+    scopedProcess.take();
+
+    connect(process, &QProcess::started,
+            this, [this]() {
+        emit outputRequested(tr("[Task (%1) started]\n").arg(getLabel()));
+    });
+    connect(process, &QProcess::readyReadStandardOutput,
+            this, [this, process]() {
+        auto text = decodeText(process->readAllStandardOutput());
+        // TODO: interaction with process.
+        emit outputRequested(text);
+    });
+    connect(process, &QProcess::readyReadStandardError,
+            this, [this, process]() {
+        auto text = process->readAllStandardError();
+        emit outputRequested(decodeText(text));
+    });
+    connect(process, &QProcess::errorOccurred,
+            this, [this](QProcess::ProcessError error) {
+        emit outputRequested(tr("[Task (%1) error occurred (%2)]\n").arg(getLabel()).arg(error));
+    });
+    connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
+            this, [this, process](int exitCode) {
+        emit outputRequested(tr("\n[Task (%1) finished (%2)]\n").arg(getLabel()).arg(exitCode));
+        process->deleteLater();
+    });
+
+    return process;
+}
+
+void Task::run()
+{
+    QProcess *process;
+    try {
+        process = setupProcess();
+    }  catch (const char *msg) {
+        qWarning() << "exception while setup process" << msg;
+        return ;
+    }
+
+    if (process) {
+        qDebug() << "run task" << process->program() << process->arguments();
+        process->start();
+    }
+}
+
+const TaskDTO &Task::getDTO() const
+{
+    return m_dto;
+}
+
+QString Task::decodeText(const QByteArray &p_text)
+{
+    static QByteArrayList codecNames = {
+        "UTF-8",
+        "System",
+        "UTF-16",
+        "GB18030"
+    };
+
+    for (const auto &name : codecNames) {
+        auto text = decodeText(p_text, name);
+        if (!text.isNull()) {
+            return text;
+        }
+    }
+
+    return QString::fromLocal8Bit(p_text);
+}
+
+QString Task::decodeText(const QByteArray &p_text, const QByteArray &p_name)
+{
+    auto codec = QTextCodec::codecForName(p_name);
+    if (codec) {
+        QTextCodec::ConverterState state;
+        auto text = codec->toUnicode(p_text.data(), p_text.size(), &state);
+        if (state.invalidChars > 0) {
+            return QString();
+        }
+        return text;
+    }
+    return QString();
+}
+
+QString Task::getLocaleString(const QJsonValue &p_value, const QString &p_locale)
+{
+    if (p_value.isObject()) {
+        auto obj = p_value.toObject();
+        if (obj.contains(p_locale)) {
+            return obj.value(p_locale).toString();
+        } else {
+            qWarning() << "value of locale not found" << p_locale;
+            if (!obj.isEmpty()){
+                return obj.begin().value().toString();
+            } else {
+                return QString();
+            }
+        }
+    } else {
+        return p_value.toString();
+    }
+}
+
+QStringList Task::getLocaleStringList(const QJsonValue &p_value, const QString &p_locale)
+{
+    QStringList strs;
+    const auto arr = p_value.toArray();
+    for (int i = 0; i < arr.size(); ++i) {
+        strs << getLocaleString(arr[i], p_locale);
+    }
+    return strs;
+}
+
+QStringList Task::getStringList(const QJsonValue &p_value)
+{
+    QStringList strs;
+    const auto arr = p_value.toArray();
+    for (int i = 0; i < arr.size(); ++i) {
+        strs << arr[i].toString();
+    }
+    return strs;
+}
+
+const TaskVariableMgr &Task::variableMgr() const
+{
+    return m_taskMgr->getVariableMgr();
+}
+
+bool Task::isCancelled() const
+{
+    return m_cancelled;
+}
+
+void Task::setCancelled(bool p_cancelled)
+{
+    m_cancelled = p_cancelled;
+}

+ 197 - 0
src/task/task.h

@@ -0,0 +1,197 @@
+#ifndef TASK_H
+#define TASK_H
+
+#include <QObject>
+
+#include <QVector>
+#include <QMap>
+#include <QSharedPointer>
+
+class QAction;
+class QProcess;
+class QJsonObject;
+
+namespace tests
+{
+    class TestTask;
+}
+
+namespace vnotex
+{
+    struct ButtonDTO
+    {
+        QString text;
+    };
+
+    struct InputDTO
+    {
+        QString id;
+
+        QString type;
+
+        QString description;
+
+        QString default_;
+
+        bool password;
+
+        QStringList options;
+    };
+
+    struct MessageDTO
+    {
+        QString id;
+
+        QString type;
+
+        QString title;
+
+        QString text;
+
+        QString detailedText;
+
+        QVector<ButtonDTO> buttons;
+    };
+
+    struct ShellOptionsDTO
+    {
+        QString executable;
+
+        QStringList args;
+    };
+
+    struct TaskOptionsDTO
+    {
+        QString cwd;
+
+        QMap<QString, QString> env;
+
+        ShellOptionsDTO shell;
+    };
+
+    struct TaskDTO
+    {
+        QString version;
+
+        QString type;
+
+        QString command;
+
+        QStringList args;
+
+        QString label;
+
+        QString icon;
+
+        QString shortcut;
+
+        QVector<InputDTO> inputs;
+
+        QVector<MessageDTO> messages;
+
+        TaskOptionsDTO options;
+
+        QString _scope;
+
+        QString _source;
+    };
+
+    class TaskMgr;
+    class TaskVariableMgr;
+
+    class Task : public QObject
+    {
+        Q_OBJECT
+    public:
+        friend class tests::TestTask;
+
+        // For top level Task, use QSharedPointer instead of QObject to manage ownership.
+        static QSharedPointer<Task> fromFile(const QString &p_file, const QString &p_locale, TaskMgr *p_taskMgr);
+
+        void run();
+
+        const TaskDTO &getDTO() const;
+
+        const QString &getVersion() const;
+
+        const QString &getType() const;
+
+        QString getCommand();
+
+        QStringList getArgs();
+
+        const QString &getLabel() const;
+
+        const QString &getIcon() const;
+
+        const QString &getShortcut() const;
+
+        QString getOptionsCwd();
+
+        const QMap<QString, QString> &getOptionsEnv() const;
+
+        const QString &getOptionsShellExecutable() const;
+
+        QStringList getOptionsShellArgs();
+
+        const QVector<Task *> &getChildren() const;
+
+        const QVector<InputDTO> &getInputs() const;
+
+        const InputDTO *findInput(const QString &p_id) const;
+
+        const MessageDTO *findMessage(const QString &p_id) const;
+
+        const QString &getFile() const;
+
+        bool isCancelled() const;
+
+        void setCancelled(bool p_cancelled);
+
+        static QString s_latestVersion;
+
+        static QString getLocaleString(const QJsonValue &p_value,
+                                       const QString &p_locale);
+
+        static QStringList getLocaleStringList(const QJsonValue &p_value,
+                                               const QString &p_locale);
+
+        static QStringList getStringList(const QJsonValue &p_value);
+
+        static QString decodeText(const QByteArray &p_text);
+
+    signals:
+        void outputRequested(const QString &p_text) const;
+
+    private:
+        Task(const QString &p_locale,
+             const QString &p_file,
+             TaskMgr *p_taskMgr,
+             QObject *p_parent = nullptr);
+
+        // Must call start() or delete the returned QProcess.
+        QProcess *setupProcess();
+
+        const TaskVariableMgr &variableMgr() const;
+
+        static bool fromJson(Task *p_task, const QJsonObject &p_obj);
+
+        static bool fromJsonV0(Task *p_task, const QJsonObject &p_obj, bool p_mergeTasks = false);
+
+        static QString decodeText(const QByteArray &p_text, const QByteArray &p_name);
+
+        Task *m_parent = nullptr;
+
+        QVector<Task *> m_children;
+
+        TaskMgr *m_taskMgr = nullptr;
+
+        TaskDTO m_dto;
+
+        QString m_locale;
+
+        bool m_cancelled = false;
+    };
+} // ns vnotex
+
+#endif // TASK_H

+ 13 - 0
src/task/task.pri

@@ -0,0 +1,13 @@
+QT += widgets
+
+SOURCES += \
+    $$PWD/task.cpp \
+    $$PWD/taskmgr.cpp \
+    $$PWD/taskvariablemgr.cpp \
+    $$PWD/shellexecution.cpp
+
+HEADERS += \
+    $$PWD/task.h \
+    $$PWD/taskmgr.h \
+    $$PWD/taskvariablemgr.h \
+    $$PWD/shellexecution.h

+ 111 - 0
src/task/taskmgr.cpp

@@ -0,0 +1,111 @@
+#include "taskmgr.h"
+
+#include <QDir>
+#include <QDebug>
+#include <QJsonDocument>
+
+#include <core/configmgr.h>
+#include <core/coreconfig.h>
+#include <core/vnotex.h>
+#include <core/notebookmgr.h>
+#include <utils/pathutils.h>
+#include <utils/fileutils.h>
+
+using namespace vnotex;
+
+TaskMgr::TaskMgr(QObject *p_parent)
+    : QObject(p_parent),
+      m_variableMgr(this)
+{
+}
+
+void TaskMgr::init()
+{
+    m_variableMgr.init();
+
+    // Load all tasks and watch the location.
+    loadAllTasks();
+
+    connect(&VNoteX::getInst().getNotebookMgr(), &NotebookMgr::currentNotebookChanged,
+            this, [this]() {
+                loadNotebookTasks();
+                emit tasksUpdated();
+            });
+}
+
+void TaskMgr::reload()
+{
+    loadAllTasks();
+}
+
+const QVector<QSharedPointer<Task>> &TaskMgr::getAppTasks() const
+{
+    return m_appTasks;
+}
+
+const QVector<QSharedPointer<Task>> &TaskMgr::getUserTasks() const
+{
+    return m_userTasks;
+}
+
+const QVector<QSharedPointer<Task>> &TaskMgr::getNotebookTasks() const
+{
+    return m_notebookTasks;
+}
+
+QString TaskMgr::getNotebookTaskFolder()
+{
+    return QString();
+}
+
+void TaskMgr::loadAllTasks()
+{
+    loadGlobalTasks();
+
+    loadNotebookTasks();
+
+    emit tasksUpdated();
+}
+
+void TaskMgr::loadNotebookTasks()
+{
+    loadTasksFromFolder(m_notebookTasks, getNotebookTaskFolder());
+}
+
+void TaskMgr::loadGlobalTasks()
+{
+    loadTasksFromFolder(m_appTasks, ConfigMgr::getInst().getAppTaskFolder());
+    loadTasksFromFolder(m_userTasks, ConfigMgr::getInst().getUserTaskFolder());
+}
+
+void TaskMgr::loadTasksFromFolder(QVector<QSharedPointer<Task>> &p_tasks, const QString &p_folder)
+{
+    p_tasks.clear();
+
+    if (p_folder.isEmpty()) {
+        return;
+    }
+
+    const auto taskFiles = FileUtils::entryListRecursively(p_folder, {"*.json"}, QDir::Files);
+    for (const auto &file : taskFiles) {
+        auto task = loadTask(file);
+        if (task) {
+            qDebug() << "loaded task" << task->getLabel();
+            connect(task.data(), &Task::outputRequested,
+                    this, &TaskMgr::taskOutputRequested);
+            p_tasks.append(task);
+        }
+    }
+}
+
+QSharedPointer<Task> TaskMgr::loadTask(const QString &p_taskFile)
+{
+    const auto localeStr = ConfigMgr::getInst().getCoreConfig().getLocaleToUse();
+    auto task = Task::fromFile(p_taskFile, localeStr, this);
+    return task;
+}
+
+const TaskVariableMgr &TaskMgr::getVariableMgr() const
+{
+    return m_variableMgr;
+}

+ 63 - 0
src/task/taskmgr.h

@@ -0,0 +1,63 @@
+#ifndef TASKMGR_H
+#define TASKMGR_H
+
+#include <QObject>
+#include <core/noncopyable.h>
+
+#include <QVector>
+#include <QSharedPointer>
+
+#include "task.h"
+#include "taskvariablemgr.h"
+
+namespace vnotex
+{
+    class TaskMgr : public QObject, private Noncopyable
+    {
+        Q_OBJECT
+    public:
+        explicit TaskMgr(QObject *p_parent = nullptr);
+
+        // It will be invoked after MainWindow show.
+        void init();
+
+        void reload();
+
+        const QVector<QSharedPointer<Task>> &getAppTasks() const;
+
+        const QVector<QSharedPointer<Task>> &getUserTasks() const;
+
+        const QVector<QSharedPointer<Task>> &getNotebookTasks() const;
+
+        static QString getNotebookTaskFolder();
+
+        const TaskVariableMgr &getVariableMgr() const;
+
+    signals:
+        void tasksUpdated();
+
+        void taskOutputRequested(const QString &p_text) const;
+
+    private:
+        void loadAllTasks();
+
+        void loadNotebookTasks();
+
+        void loadGlobalTasks();
+
+        void loadTasksFromFolder(QVector<QSharedPointer<Task>> &p_tasks, const QString &p_folder);
+
+        // Return nullptr if not a valid task.
+        QSharedPointer<Task> loadTask(const QString &p_taskFile);
+
+        QVector<QSharedPointer<Task>> m_appTasks;
+
+        QVector<QSharedPointer<Task>> m_userTasks;
+
+        QVector<QSharedPointer<Task>> m_notebookTasks;
+
+        TaskVariableMgr m_variableMgr;
+    };
+} // ns vnotex
+
+#endif // TASKMGR_H

+ 435 - 0
src/task/taskvariablemgr.cpp

@@ -0,0 +1,435 @@
+#include "taskvariablemgr.h"
+
+#include <QRegularExpression>
+#include <QInputDialog>
+#include <QApplication>
+#include <QRandomGenerator>
+#include <QTimeZone>
+#include <QProcess>
+
+#include <core/vnotex.h>
+#include <core/notebookmgr.h>
+#include <core/configmgr.h>
+#include <core/mainconfig.h>
+#include <core/exception.h>
+#include <widgets/mainwindow.h>
+#include <widgets/dialogs/selectdialog.h>
+#include <notebook/notebook.h>
+#include <notebook/node.h>
+#include <utils/pathutils.h>
+#include <buffer/buffer.h>
+#include <widgets/viewwindow.h>
+#include <widgets/viewarea.h>
+#include <snippet/snippetmgr.h>
+
+#include "task.h"
+#include "taskmgr.h"
+#include "shellexecution.h"
+
+using namespace vnotex;
+
+
+TaskVariable::TaskVariable(const QString &p_name, const Func &p_func)
+    : m_name(p_name),
+      m_func(p_func)
+{
+}
+
+QString TaskVariable::evaluate(Task *p_task, const QString &p_value) const
+{
+    return m_func(p_task, p_value);
+}
+
+
+const QString TaskVariableMgr::c_variableSymbolRegExp = QString(R"(\$\{([^${}:]+)(?::([^${}:]+))?\})");
+
+TaskVariableMgr::TaskVariableMgr(TaskMgr *p_taskMgr)
+    : m_taskMgr(p_taskMgr)
+{
+}
+
+void TaskVariableMgr::init()
+{
+    initVariables();
+}
+
+void TaskVariableMgr::initVariables()
+{
+    m_variables.clear();
+
+    m_needUpdateSystemEnvironment = true;
+
+    initNotebookVariables();
+
+    initBufferVariables();
+
+    initTaskVariables();
+
+    initMagicVariables();
+
+    initEnvironmentVariables();
+
+    initConfigVariables();
+
+    initInputVariables();
+
+    initShellVariables();
+}
+
+void TaskVariableMgr::initNotebookVariables()
+{
+    addVariable("notebookFolder", [](Task *, const QString &) {
+        auto notebook = TaskVariableMgr::getCurrentNotebook();
+        if (notebook) {
+            return PathUtils::cleanPath(notebook->getRootFolderAbsolutePath());
+        } else {
+            return QString();
+        }
+    });
+    addVariable("notebookFolderName", [](Task *, const QString &) {
+        auto notebook = TaskVariableMgr::getCurrentNotebook();
+        if (notebook) {
+            return PathUtils::dirName(notebook->getRootFolderPath());
+        } else {
+            return QString();
+        }
+    });
+    addVariable("notebookName", [](Task *, const QString &) {
+        auto notebook = TaskVariableMgr::getCurrentNotebook();
+        if (notebook) {
+            return notebook->getName();
+        } else {
+            return QString();
+        }
+    });
+    addVariable("notebookDescription", [](Task *, const QString &) {
+        auto notebook = TaskVariableMgr::getCurrentNotebook();
+        if (notebook) {
+            return notebook->getDescription();
+        } else {
+            return QString();
+        }
+    });
+}
+
+void TaskVariableMgr::initBufferVariables()
+{
+    addVariable("buffer", [](Task *, const QString &) {
+        auto buffer = getCurrentBuffer();
+        if (buffer) {
+            return PathUtils::cleanPath(buffer->getPath());
+        }
+        return QString();
+    });
+    addVariable("bufferNotebookFolder", [](Task *, const QString &) {
+        auto buffer = getCurrentBuffer();
+        if (buffer) {
+            auto node = buffer->getNode();
+            if (node) {
+                return PathUtils::cleanPath(node->getNotebook()->getRootFolderAbsolutePath());
+            }
+        }
+        return QString();
+    });
+    addVariable("bufferRelativePath", [](Task *, const QString &) {
+        auto buffer = getCurrentBuffer();
+        if (buffer) {
+            auto node = buffer->getNode();
+            if (node) {
+                return PathUtils::cleanPath(node->fetchPath());
+            } else {
+                return PathUtils::cleanPath(buffer->getPath());
+            }
+        }
+        return QString();
+    });
+    addVariable("bufferName", [](Task *, const QString &) {
+        auto buffer = getCurrentBuffer();
+        if (buffer) {
+            return PathUtils::fileName(buffer->getPath());
+        }
+        return QString();
+    });
+    addVariable("bufferBaseName", [](Task *, const QString &) {
+        auto buffer = getCurrentBuffer();
+        if (buffer) {
+            return QFileInfo(buffer->getPath()).completeBaseName();
+        }
+        return QString();
+    });
+    addVariable("bufferDir", [](Task *, const QString &) {
+        auto buffer = getCurrentBuffer();
+        if (buffer) {
+            return PathUtils::parentDirPath(buffer->getPath());
+        }
+        return QString();
+    });
+    addVariable("bufferExt", [](Task *, const QString &) {
+        auto buffer = getCurrentBuffer();
+        if (buffer) {
+            return QFileInfo(buffer->getPath()).suffix();
+        }
+        return QString();
+    });
+    addVariable("selectedText", [](Task *, const QString &) {
+        auto win = getCurrentViewWindow();
+        if (win) {
+            return win->selectedText();
+        }
+        return QString();
+    });
+}
+
+void TaskVariableMgr::initTaskVariables()
+{
+    addVariable("cwd", [](Task *task, const QString &) {
+        return PathUtils::cleanPath(task->getOptionsCwd());
+    });
+    addVariable("taskFile", [](Task *task, const QString &) {
+        return PathUtils::cleanPath(task->getFile());
+    });
+    addVariable("taskDir", [](Task *task, const QString &) {
+        return PathUtils::parentDirPath(task->getFile());
+    });
+    addVariable("exeFile", [](Task *, const QString &) {
+        return PathUtils::cleanPath(qApp->applicationFilePath());
+    });
+    addVariable("pathSeparator", [](Task *, const QString &) {
+        return QDir::separator();
+    });
+    addVariable("notebookTaskFolder", [this](Task *, const QString &) {
+        return PathUtils::cleanPath(m_taskMgr->getNotebookTaskFolder());
+    });
+    addVariable("userTaskFolder", [](Task *, const QString &) {
+        return PathUtils::cleanPath(ConfigMgr::getInst().getUserTaskFolder());
+    });
+    addVariable("appTaskFolder", [](Task *, const QString &) {
+        return PathUtils::cleanPath(ConfigMgr::getInst().getAppTaskFolder());
+    });
+    addVariable("userThemeFolder", [](Task *, const QString &) {
+        return PathUtils::cleanPath(ConfigMgr::getInst().getUserThemeFolder());
+    });
+    addVariable("appThemeFolder", [](Task *, const QString &) {
+        return PathUtils::cleanPath(ConfigMgr::getInst().getAppThemeFolder());
+    });
+    addVariable("userDocsFolder", [](Task *, const QString &) {
+        return PathUtils::cleanPath(ConfigMgr::getInst().getUserDocsFolder());
+    });
+    addVariable("appDocsFolder", [](Task *, const QString &) {
+        return PathUtils::cleanPath(ConfigMgr::getInst().getAppDocsFolder());
+    });
+}
+
+void TaskVariableMgr::initMagicVariables()
+{
+    addVariable("magic", [](Task *, const QString &val) {
+        if (val.isEmpty()) {
+            return QString();
+        }
+
+        auto overrides = SnippetMgr::generateOverrides(getCurrentBuffer());
+        return SnippetMgr::getInst().applySnippetBySymbol(SnippetMgr::generateSnippetSymbol(val), overrides);
+    });
+}
+
+void TaskVariableMgr::initEnvironmentVariables()
+{
+    addVariable("env", [this](Task *, const QString &val) {
+        if (val.isEmpty()) {
+            return QString();
+        }
+        if (m_needUpdateSystemEnvironment) {
+            m_needUpdateSystemEnvironment = false;
+            m_systemEnv = QProcessEnvironment::systemEnvironment();
+        }
+        return m_systemEnv.value(val);
+    });
+}
+
+void TaskVariableMgr::initConfigVariables()
+{
+    // ${config:main.core.shortcuts.FullScreen}.
+    addVariable("config", [](Task *, const QString &val) {
+        if (val.isEmpty()) {
+            return QString();
+        }
+        auto jsonVal = ConfigMgr::getInst().parseAndReadConfig(val);
+        switch (jsonVal.type()) {
+        case QJsonValue::Bool:
+            return jsonVal.toBool() ? QStringLiteral("1") : QStringLiteral("0");
+        break;
+
+        case QJsonValue::Double:
+            return QString::number(jsonVal.toDouble());
+        break;
+
+        case QJsonValue::String:
+            return jsonVal.toString();
+        break;
+
+        default:
+            return QString();
+        }
+    });
+}
+
+void TaskVariableMgr::initInputVariables()
+{
+    // ${input:inputId}.
+    addVariable("input", [this](Task *task, const QString &val) {
+        if (val.isEmpty()) {
+            Exception::throwOne(Exception::Type::InvalidArgument,
+                                QString("task (%1) with empty input id").arg(task->getLabel()));
+        }
+
+        auto input = task->findInput(val);
+        if (!input) {
+            Exception::throwOne(Exception::Type::InvalidArgument,
+                                QString("task (%1) with invalid input id (%2)").arg(task->getLabel(), val));
+        }
+
+        if (input->type == "promptString") {
+            const auto desc = evaluate(task, input->description);
+            const auto defaultText = evaluate(task, input->default_);
+            QInputDialog dialog(VNoteX::getInst().getMainWindow());
+            dialog.setInputMode(QInputDialog::TextInput);
+            dialog.setTextEchoMode(input->password ? QLineEdit::Password : QLineEdit::Normal);
+            dialog.setWindowTitle(task->getLabel());
+            dialog.setLabelText(desc);
+            dialog.setTextValue(defaultText);
+            if (dialog.exec() == QDialog::Accepted) {
+                return dialog.textValue();
+            } else {
+                task->setCancelled(true);
+                return QString();
+            }
+        } else if (input->type == "pickString") {
+            const auto desc = evaluate(task, input->description);
+            SelectDialog dialog(task->getLabel(), desc, VNoteX::getInst().getMainWindow());
+            for (int i = 0; i < input->options.size(); i++) {
+                dialog.addSelection(input->options.at(i), i);
+            }
+
+            if (dialog.exec() == QDialog::Accepted) {
+                int selection = dialog.getSelection();
+                return input->options.at(selection);
+            } else {
+                task->setCancelled(true);
+                return QString();
+            }
+        } else {
+            Exception::throwOne(Exception::Type::InvalidArgument,
+                                QString("task (%1) with invalid input type (%2)(%3)").arg(task->getLabel(), input->id, input->type));
+        }
+
+        return QString();
+    });
+}
+
+void TaskVariableMgr::initShellVariables()
+{
+    // ${shell:command}.
+    addVariable("shell", [this](Task *task, const QString &val) {
+        QProcess process;
+        process.setWorkingDirectory(task->getOptionsCwd());
+        ShellExecution::setupProcess(&process, val);
+        process.start();
+        const int timeout = 1000;
+        if (!process.waitForStarted(timeout) || !process.waitForFinished(timeout)) {
+            Exception::throwOne(Exception::Type::InvalidArgument,
+                                QString("task (%1) failed to fetch shell variable (%2)").arg(task->getLabel(), val));
+        }
+        return Task::decodeText(process.readAllStandardOutput());
+    });
+}
+
+void TaskVariableMgr::addVariable(const QString &p_name, const TaskVariable::Func &p_func)
+{
+    Q_ASSERT(!m_variables.contains(p_name));
+
+    m_variables.insert(p_name, TaskVariable(p_name, p_func));
+}
+
+const ViewWindow *TaskVariableMgr::getCurrentViewWindow()
+{
+    return VNoteX::getInst().getMainWindow()->getViewArea()->getCurrentViewWindow();
+}
+
+Buffer *TaskVariableMgr::getCurrentBuffer()
+{
+    auto win = getCurrentViewWindow();
+    if (win) {
+        return win->getBuffer();
+    }
+    return nullptr;
+}
+
+QSharedPointer<Notebook> TaskVariableMgr::getCurrentNotebook()
+{
+    return VNoteX::getInst().getNotebookMgr().getCurrentNotebook();
+}
+
+QString TaskVariableMgr::evaluate(Task *p_task, const QString &p_text) const
+{
+    QString content(p_text);
+
+    int maxTimesAtSamePos = 100;
+
+    QRegularExpression regExp(c_variableSymbolRegExp);
+    int pos = 0;
+    while (pos < content.size()) {
+        QRegularExpressionMatch match;
+        int idx = content.indexOf(regExp, pos, &match);
+        if (idx == -1) {
+            break;
+        }
+
+        const auto varName = match.captured(1).trimmed();
+        const auto varValue = match.captured(2).trimmed();
+        auto var = findVariable(varName);
+        if (!var) {
+            // Skip it.
+            pos = idx + match.capturedLength(0);
+            continue;
+        }
+
+        const auto afterText = var->evaluate(p_task, varValue);
+        content.replace(idx, match.capturedLength(0), afterText);
+
+        // @afterText may still contains variable symbol.
+        if (pos == idx) {
+            if (--maxTimesAtSamePos == 0) {
+                break;
+            }
+        } else {
+            maxTimesAtSamePos = 100;
+        }
+        pos = idx;
+    }
+
+    return content;
+}
+
+QStringList TaskVariableMgr::evaluate(Task *p_task, const QStringList &p_texts) const
+{
+    QStringList strs;
+    for (const auto &str : p_texts) {
+        strs << evaluate(p_task, str);
+    }
+    return strs;
+}
+
+const TaskVariable *TaskVariableMgr::findVariable(const QString &p_name) const
+{
+    auto it = m_variables.find(p_name);
+    if (it != m_variables.end()) {
+        return &(it.value());
+    }
+
+    return nullptr;
+}
+
+void TaskVariableMgr::overrideVariable(const QString &p_name, const TaskVariable::Func &p_func)
+{
+    m_variables.insert(p_name, TaskVariable(p_name, p_func));
+}

+ 105 - 0
src/task/taskvariablemgr.h

@@ -0,0 +1,105 @@
+#ifndef TASKVARIABLEMGR_H
+#define TASKVARIABLEMGR_H
+
+#include <core/noncopyable.h>
+
+#include <functional>
+
+#include <QHash>
+#include <QString>
+#include <QSharedPointer>
+#include <QScopedPointer>
+#include <QProcessEnvironment>
+
+namespace vnotex
+{
+    class Task;
+    class Notebook;
+    class Buffer;
+    class ViewWindow;
+    class TaskMgr;
+
+    class TaskVariable
+    {
+    public:
+        typedef std::function<QString(Task *, const QString &)> Func;
+
+        TaskVariable(const QString &p_name, const Func &p_func);
+
+        QString evaluate(Task *p_task, const QString &p_value) const;
+
+    private:
+        QString m_name;
+
+        Func m_func;
+    };
+
+
+    class TaskVariableMgr : private Noncopyable
+    {
+    public:
+        explicit TaskVariableMgr(TaskMgr *p_taskMgr);
+
+        void init();
+
+        QString evaluate(Task *p_task, const QString &p_text) const;
+
+        QStringList evaluate(Task *p_task, const QStringList &p_texts) const;
+
+        // Used for UT.
+        void overrideVariable(const QString &p_name, const TaskVariable::Func &p_func);
+
+        static Buffer *getCurrentBuffer();
+
+        static QSharedPointer<Notebook> getCurrentNotebook();
+
+    private:
+        void initVariables();
+
+        void initNotebookVariables();
+
+        void initBufferVariables();
+
+        void initTaskVariables();
+
+        void initMagicVariables();
+
+        void initEnvironmentVariables();
+
+        void initConfigVariables();
+
+        void initInputVariables();
+
+        void initShellVariables();
+
+        void addVariable(const QString &p_name, const TaskVariable::Func &p_func);
+
+        const TaskVariable *findVariable(const QString &p_name) const;
+
+        /*
+        QString evaluateInputVariables(const QString &p_text,
+                                       const Task *p_task) const;
+
+        QString evaluateShellVariables(const QString &p_text,
+                                       const Task *p_task) const;
+       */
+
+        static const ViewWindow *getCurrentViewWindow();
+
+        TaskMgr *m_taskMgr = nullptr;
+
+        QHash<QString, TaskVariable> m_variables;
+
+        bool m_needUpdateSystemEnvironment = true;
+
+        QProcessEnvironment m_systemEnv;
+
+        // %{name[:value]}%.
+        // Captured texts:
+        // 1 - The name of the variable (trim needed).
+        // 2 - The value option of the variable if available (trim needed).
+        static const QString c_variableSymbolRegExp;
+    };
+} // ns vnotex
+
+#endif // TASKVARIABLEMGR_H

+ 14 - 8
src/utils/fileutils.cpp

@@ -362,17 +362,23 @@ QStringList FileUtils::entryListRecursively(const QString &p_dirPath,
                                             const QStringList &p_nameFilters,
                                             QDir::Filters p_filters)
 {
+    QStringList entries;
+
     QDir dir(p_dirPath);
-    if (dir.isEmpty()) return {};
-    QStringList entrys;
-    const auto curEntrys = dir.entryList(p_nameFilters, p_filters | QDir::NoDotAndDotDot);
-    for (const auto &e : curEntrys) {
-        entrys.append(PathUtils::concatenateFilePath(p_dirPath, e));
+    if (!dir.exists()) {
+        return entries;
+    }
+
+    const auto curEntries = dir.entryList(p_nameFilters, p_filters | QDir::NoDotAndDotDot);
+    for (const auto &e : curEntries) {
+        entries.append(PathUtils::concatenateFilePath(p_dirPath, e));
     }
-    auto subdirs = dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
+
+    const auto subdirs = dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
     for (const auto &subdir : subdirs) {
         const auto dirPath = PathUtils::concatenateFilePath(p_dirPath, subdir);
-        entrys.append(entryListRecursively(dirPath, p_nameFilters, p_filters));
+        entries.append(entryListRecursively(dirPath, p_nameFilters, p_filters));
     }
-    return entrys;
+
+    return entries;
 }

+ 1 - 1
src/utils/fileutils.h

@@ -83,7 +83,7 @@ namespace vnotex
         // @p_nameFilters is for each dir, not for all.
         static QStringList entryListRecursively(const QString &p_dirPath,
                                                 const QStringList &p_nameFilters,
-                                                QDir::Filters p_filters=QDir::NoFilter);
+                                                QDir::Filters p_filters = QDir::NoFilter);
     };
 } // ns vnotex
 

+ 0 - 1
src/utils/pathutils.cpp

@@ -16,7 +16,6 @@ QString PathUtils::parentDirPath(const QString &p_path)
     }
 
     QFileInfo info(p_path);
-    Q_ASSERT(info.isAbsolute());
     return cleanPath(info.absolutePath());
 }
 

+ 54 - 1
src/utils/utils.cpp

@@ -4,13 +4,15 @@
 #include <QDir>
 #include <QKeySequence>
 #include <QWidget>
-#include <QVariant>
 #include <QFontDatabase>
 #include <QRegularExpression>
+#include <QRegularExpressionMatch>
 #include <QSvgRenderer>
 #include <QPainter>
 #include <QJsonObject>
+#include <QJsonArray>
 #include <QJsonDocument>
+#include <QDebug>
 
 #include <cmath>
 
@@ -139,3 +141,54 @@ QJsonObject Utils::fromJsonString(const QByteArray &p_data)
 {
     return QJsonDocument::fromJson(p_data).object();
 }
+
+QJsonValue Utils::parseAndReadJson(const QJsonObject &p_obj, const QString &p_exp)
+{
+    // abc[0] or abc.
+    QRegularExpression regExp(R"(^([^\[\]\s]+)(?:\[(\d+)\])?$)");
+
+    QJsonValue val(p_obj);
+
+    bool valid = true;
+    const auto tokens = p_exp.split(QLatin1Char('.'));
+    for (int i = 0; i < tokens.size(); ++i) {
+        const auto &token = tokens[i];
+        if (token.isEmpty()) {
+            continue;
+        }
+
+        auto match = regExp.match(token);
+        if (!match.hasMatch()) {
+            valid = false;
+            break;
+        }
+
+        const auto key = match.captured(1);
+        const auto obj = val.toObject();
+        if (obj.contains(key)) {
+            val = obj.value(key);
+        } else {
+            valid = false;
+            break;
+        }
+
+        if (!match.captured(2).isEmpty()) {
+            // Array.
+            const auto arr = val.toArray();
+            int idx = match.captured(2).toInt();
+            if (idx < 0 || idx >= arr.size()) {
+                valid = false;
+                break;
+            }
+
+            val = arr[idx];
+        }
+    }
+
+    if (!valid) {
+        qWarning() << "invalid expression to parse for JSON" << p_exp;
+        return QJsonValue();
+    }
+
+    return val;
+}

+ 4 - 0
src/utils/utils.h

@@ -57,6 +57,10 @@ namespace vnotex
         static QByteArray toJsonString(const QJsonObject &p_obj);
 
         static QJsonObject fromJsonString(const QByteArray &p_data);
+
+        // Parse @p_exp into tokens and read the target value from @p_obj.
+        // Format: obj1.obj2.arr[2].obj3.
+        static QJsonValue parseAndReadJson(const QJsonObject &p_obj, const QString &p_exp);
     };
 } // ns vnotex
 

+ 1 - 6
src/widgets/dialogs/selectdialog.cpp

@@ -13,13 +13,8 @@ using namespace vnotex;
 const QChar SelectDialog::c_cancelShortcut = QLatin1Char('z');
 
 SelectDialog::SelectDialog(const QString &p_title, QWidget *p_parent)
-    : QDialog(p_parent)
+    : SelectDialog(p_title, QString(), p_parent)
 {
-    const auto &themeMgr = VNoteX::getInst().getThemeMgr();
-    m_shortcutIconForeground = themeMgr.paletteColor(QStringLiteral("widgets#quickselector#item_icon#fg"));
-    m_shortcutIconBorder = themeMgr.paletteColor(QStringLiteral("widgets#quickselector#item_icon#border"));
-
-    setupUI(p_title);
 }
 
 SelectDialog::SelectDialog(const QString &p_title,

+ 9 - 8
src/widgets/dockwidgethelper.cpp

@@ -25,6 +25,7 @@
 #include "snippetpanel.h"
 #include "historypanel.h"
 #include "tagexplorer.h"
+#include "terminalviewer.h"
 
 using namespace vnotex;
 
@@ -113,7 +114,7 @@ void DockWidgetHelper::setupDocks()
 
     setupOutlineDock();
 
-    setupOutputDock();
+    setupTerminalDock();
 
     setupLocationListDock();
 
@@ -148,17 +149,17 @@ void DockWidgetHelper::setupOutlineDock()
     m_mainWindow->addDockWidget(Qt::RightDockWidgetArea, dock);
 }
 
-void DockWidgetHelper::setupOutputDock()
+void DockWidgetHelper::setupTerminalDock()
 {
-    auto dock = createDockWidget(DockIndex::OutputDock, tr("Output"), m_mainWindow);
+    auto dock = createDockWidget(DockIndex::TerminalDock, tr("Terminal"), m_mainWindow);
 
-    dock->setObjectName(QStringLiteral("OutputDock.vnotex"));
-    dock->setAllowedAreas(Qt::BottomDockWidgetArea);
+    dock->setObjectName(QStringLiteral("TerminalDock.vnotex"));
+    dock->setAllowedAreas(Qt::AllDockWidgetAreas);
 
-    dock->setWidget(m_mainWindow->m_outputViewer);
-    dock->setFocusProxy(m_mainWindow->m_outputViewer);
-    dock->hide();
+    dock->setWidget(m_mainWindow->m_terminalViewer);
+    dock->setFocusProxy(m_mainWindow->m_terminalViewer);
     m_mainWindow->addDockWidget(Qt::BottomDockWidgetArea, dock);
+    dock->hide();
 }
 
 void DockWidgetHelper::setupSearchDock()

+ 2 - 2
src/widgets/dockwidgethelper.h

@@ -29,7 +29,7 @@ namespace vnotex
             SearchDock,
             SnippetDock,
             OutlineDock,
-            OutputDock,
+            TerminalDock,
             LocationListDock,
             MaxDock
         };
@@ -98,7 +98,7 @@ namespace vnotex
 
         void setupOutlineDock();
 
-        void setupOutputDock();
+        void setupTerminalDock();
 
         void setupSearchDock();
 

+ 0 - 6
src/widgets/framelessmainwindow/framelessmainwindowlinux.cpp

@@ -230,10 +230,4 @@ void FramelessMainWindowLinux::showEvent(QShowEvent *p_event)
     }
 }
 
-#else
-using namespace vnotex;
-
-FramelessMainWindowLinuxDummy::FramelessMainWindowLinuxDummy()
-{
-}
 #endif

+ 1 - 1
src/widgets/framelessmainwindow/framelessmainwindowlinux.h

@@ -65,7 +65,7 @@ namespace vnotex
     class FramelessMainWindowLinuxDummy
     {
     public:
-        FramelessMainWindowLinuxDummy();
+        FramelessMainWindowLinuxDummy() = default;
     };
 #endif
 }

+ 0 - 6
src/widgets/framelessmainwindow/framelessmainwindowwin.cpp

@@ -221,10 +221,4 @@ void FramelessMainWindowWin::forceRedraw()
     }
 }
 
-#else
-using namespace vnotex;
-
-FramelessMainWindowWinDummy::FramelessMainWindowWinDummy()
-{
-}
 #endif

+ 1 - 1
src/widgets/framelessmainwindow/framelessmainwindowwin.h

@@ -41,7 +41,7 @@ namespace vnotex
     class FramelessMainWindowWinDummy
     {
     public:
-        FramelessMainWindowWinDummy();
+        FramelessMainWindowWinDummy() = default;
     };
 #endif
 }

+ 8 - 15
src/widgets/mainwindow.cpp

@@ -56,6 +56,7 @@
 #include "tagexplorer.h"
 #include "toolbarhelper.h"
 #include "statusbarhelper.h"
+#include "terminalviewer.h"
 
 using namespace vnotex;
 
@@ -257,7 +258,7 @@ void MainWindow::setupDocks()
 
     setupOutlineViewer();
 
-    setupOutputViewer();
+    setupTerminalViewer();
 
     setupHistoryPanel();
 
@@ -514,23 +515,15 @@ void MainWindow::setupOutlineViewer()
             this, &MainWindow::focusViewArea);
 }
 
-void MainWindow::setupOutputViewer()
+void MainWindow::setupTerminalViewer()
 {
-    m_outputViewer = new QTextEdit(this);
-    m_outputViewer->setObjectName("OutputViewer.vnotex");
-    m_outputViewer->setReadOnly(true);
+    m_terminalViewer = new TerminalViewer(this);
+    m_terminalViewer->setObjectName("TerminalViewer.vnotex");
 
     connect(&VNoteX::getInst(), &VNoteX::showOutputRequested,
-            m_outputViewer, [this](const QString &p_text) {
-        auto cursor = m_outputViewer->textCursor();
-        cursor.movePosition(QTextCursor::End);
-        m_outputViewer->setTextCursor(cursor);
-        m_outputViewer->insertPlainText(p_text);
-        auto scrollBar = m_outputViewer->verticalScrollBar();
-        if (scrollBar) {
-            scrollBar->setSliderPosition(scrollBar->maximum());
-        }
-        m_dockWidgetHelper.getDock(DockWidgetHelper::OutputDock)->show();
+            this, [this](const QString &p_text) {
+        m_terminalViewer->append(p_text);
+        m_dockWidgetHelper.activateDock(DockWidgetHelper::TerminalDock);
     });
 }
 

+ 3 - 2
src/widgets/mainwindow.h

@@ -28,6 +28,7 @@ namespace vnotex
     class SnippetPanel;
     class HistoryPanel;
     class ExportDialog;
+    class TerminalViewer;
 
     enum { RESTART_EXIT_CODE = 1000 };
 
@@ -106,7 +107,7 @@ namespace vnotex
 
         void setupOutlineViewer();
 
-        void setupOutputViewer();
+        void setupTerminalViewer();
 
         void setupSearchPanel();
 
@@ -167,7 +168,7 @@ namespace vnotex
 
         OutlineViewer *m_outlineViewer = nullptr;
 
-        QTextEdit *m_outputViewer = nullptr;
+        TerminalViewer *m_terminalViewer = nullptr;
 
         LocationList *m_locationList = nullptr;
 

+ 1 - 1
src/widgets/notebooknodeexplorer.cpp

@@ -838,7 +838,7 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
         break;
 
     case Action::OpenLocation:
-        act = new QAction(tr("Open &Location"), p_parent);
+        act = new QAction(tr("Open Locat&ion"), p_parent);
         connect(act, &QAction::triggered,
                 this, [this]() {
                     auto item = m_masterExplorer->currentItem();

+ 62 - 0
src/widgets/terminalviewer.cpp

@@ -0,0 +1,62 @@
+#include "terminalviewer.h"
+
+#include <QVBoxLayout>
+#include <QPlainTextEdit>
+#include <QScrollBar>
+#include <QToolButton>
+
+#include <utils/widgetutils.h>
+
+#include "widgetsfactory.h"
+#include "titlebar.h"
+
+using namespace vnotex;
+
+TerminalViewer::TerminalViewer(QWidget *p_parent)
+    : QFrame(p_parent)
+{
+    setupUI();
+}
+
+void TerminalViewer::setupUI()
+{
+    auto mainLayout = new QVBoxLayout(this);
+    WidgetUtils::setContentsMargins(mainLayout);
+
+    {
+        setupTitleBar(QString(), this);
+        mainLayout->addWidget(m_titleBar);
+    }
+
+    m_consoleEdit = new QPlainTextEdit(this);
+    m_consoleEdit->setReadOnly(true);
+    mainLayout->addWidget(m_consoleEdit);
+
+    setFocusProxy(m_consoleEdit);
+}
+
+void TerminalViewer::setupTitleBar(const QString &p_title, QWidget *p_parent)
+{
+    m_titleBar = new TitleBar(p_title, true, TitleBar::Action::None, p_parent);
+    m_titleBar->setActionButtonsAlwaysShown(true);
+
+    {
+        auto clearBtn = m_titleBar->addActionButton(QStringLiteral("clear.svg"), tr("Clear"));
+        connect(clearBtn, &QToolButton::triggered,
+                this, &TerminalViewer::clear);
+    }
+}
+
+void TerminalViewer::append(const QString &p_text)
+{
+    m_consoleEdit->appendPlainText(p_text);
+    auto scrollBar = m_consoleEdit->verticalScrollBar();
+    if (scrollBar) {
+        scrollBar->setValue(scrollBar->maximum());
+    }
+}
+
+void TerminalViewer::clear()
+{
+    m_consoleEdit->clear();
+}

+ 33 - 0
src/widgets/terminalviewer.h

@@ -0,0 +1,33 @@
+#ifndef TERMINALVIEWER_H
+#define TERMINALVIEWER_H
+
+#include <QFrame>
+
+class QPlainTextEdit;
+
+namespace vnotex
+{
+    class TitleBar;
+
+    class TerminalViewer : public QFrame
+    {
+        Q_OBJECT
+    public:
+        explicit TerminalViewer(QWidget *p_parent = nullptr);
+
+        void append(const QString &p_text);
+
+        void clear();
+
+    private:
+        void setupUI();
+
+        void setupTitleBar(const QString &p_title, QWidget *p_parent = nullptr);
+
+        TitleBar *m_titleBar = nullptr;
+
+        QPlainTextEdit *m_consoleEdit = nullptr;
+    };
+}
+
+#endif // TERMINALVIEWER_H

+ 68 - 25
src/widgets/toolbarhelper.cpp

@@ -27,6 +27,7 @@
 #include <core/fileopenparameters.h>
 #include <core/htmltemplatehelper.h>
 #include <core/exception.h>
+#include <task/taskmgr.h>
 #include "propertydefs.h"
 #include "dialogs/settings/settingsdialog.h"
 #include "dialogs/updater.h"
@@ -286,52 +287,80 @@ QToolBar *ToolBarHelper::setupQuickAccessToolBar(MainWindow *p_win, QToolBar *p_
 void ToolBarHelper::setupTaskMenu(QMenu *p_menu)
 {
     p_menu->clear();
+
+    setupTaskActionMenu(p_menu);
+
+    p_menu->addSeparator();
+
     const auto &taskMgr = VNoteX::getInst().getTaskMgr();
-    for (auto task : taskMgr.getAppTasks()) {
-        addTaskMenu(p_menu, task);
+    for (const auto &task : taskMgr.getAppTasks()) {
+        addTaskMenu(p_menu, task.data());
     }
+
     p_menu->addSeparator();
-    for (auto task : taskMgr.getUserTasks()) {
-        addTaskMenu(p_menu, task);
+
+    for (const auto &task : taskMgr.getUserTasks()) {
+        addTaskMenu(p_menu, task.data());
     }
+
     p_menu->addSeparator();
-    for (auto task : taskMgr.getNotebookTasks()) {
-        addTaskMenu(p_menu, task);
+
+    for (const auto &task : taskMgr.getNotebookTasks()) {
+        addTaskMenu(p_menu, task.data());
     }
 }
 
+void ToolBarHelper::setupTaskActionMenu(QMenu *p_menu)
+{
+    p_menu->addAction(MainWindow::tr("Add Task"),
+                      p_menu,
+                      []() {
+                          WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(ConfigMgr::getInst().getUserTaskFolder()));
+                      });
+
+    p_menu->addAction(MainWindow::tr("Reload"),
+                      p_menu,
+                      []() {
+                          VNoteX::getInst().getTaskMgr().reload();
+                      });
+}
+
 void ToolBarHelper::addTaskMenu(QMenu *p_menu, Task *p_task)
 {
-    MainWindow::connect(p_task, &Task::showOutput,
-                        &VNoteX::getInst(), &VNoteX::showOutputRequested);
     QAction *action = nullptr;
-    const auto &tasks = p_task->getTasks();
+
+    const auto &children = p_task->getChildren();
+
     auto label = p_task->getLabel();
-    label = label.replace("&", "&&");
+    // '&' will be considered shortuct symbol in QAction.
+    label.replace("&", "&&");
+
+    if (children.isEmpty()) {
+        action = p_menu->addAction(label);
+    } else {
+        auto subMenu = p_menu->addMenu(label);
+        for (auto task : children) {
+            addTaskMenu(subMenu, task);
+        }
+        action = subMenu->menuAction();
+    }
+
     QIcon icon;
     try {
         auto taskIcon = p_task->getIcon();
         if (!taskIcon.isEmpty()) {
             icon = generateIcon(p_task->getIcon());
         }
-    }  catch (Exception e) {
+    }  catch (Exception &e) {
         if (e.m_type != Exception::Type::FailToReadFile) {
-            throw;
-        }
-    }
-    if (tasks.isEmpty()) {
-        action = p_menu->addAction(label);
-    } else {
-        auto menu = p_menu->addMenu(label);
-        for (auto task : tasks) {
-            addTaskMenu(menu, task);
+            throw e;
         }
-        action = menu->menuAction();
     }
     action->setIcon(icon);
+
+    action->setData(reinterpret_cast<qulonglong>(p_task));
+
     WidgetUtils::addActionShortcut(action, p_task->getShortcut());
-    MainWindow::connect(action, &QAction::triggered,
-                        p_task, &Task::run);
 }
 
 QToolBar *ToolBarHelper::setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar)
@@ -347,11 +376,19 @@ QToolBar *ToolBarHelper::setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar
     btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
 
     auto taskMenu = WidgetsFactory::createMenu(tb);
-    MainWindow::connect(&VNoteX::getInst().getTaskMgr(), &TaskMgr::taskChanged,
+    setupTaskActionMenu(taskMenu);
+    btn->setMenu(taskMenu);
+    MainWindow::connect(taskMenu, &QMenu::triggered,
+                        taskMenu, [](QAction *act) {
+        auto task = reinterpret_cast<Task *>(act->data().toULongLong());
+        Q_ASSERT(task);
+        task->run();
+    });
+    MainWindow::connect(&VNoteX::getInst().getTaskMgr(), &TaskMgr::tasksUpdated,
                         taskMenu, [taskMenu]() {
         setupTaskMenu(taskMenu);
     });
-    btn->setMenu(taskMenu);
+
     return tb;
 }
 
@@ -685,6 +722,12 @@ void ToolBarHelper::setupMenuButton(MainWindow *p_win, QToolBar *p_toolBar)
                                 WidgetUtils::openUrlByDesktop(QUrl("https://vnotex.github.io/vnote"));
                             });
 
+        helpMenu->addAction(MainWindow::tr("Documentation"),
+                            helpMenu,
+                            []() {
+                                WidgetUtils::openUrlByDesktop(QUrl("https://vnotex.github.io/vnote/en_us/#!docs/vx.json"));
+                            });
+
         helpMenu->addAction(MainWindow::tr("Feedback and Discussions"),
                             helpMenu,
                             []() {

+ 2 - 0
src/widgets/toolbarhelper.h

@@ -36,6 +36,8 @@ namespace vnotex
 
         static void setupTaskMenu(QMenu *p_menu);
 
+        static void setupTaskActionMenu(QMenu *p_menu);
+
         static void addTaskMenu(QMenu *p_menu, Task *p_task);
 
         static QToolBar *setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar);

+ 2 - 2
src/widgets/viewwindow.h

@@ -97,6 +97,8 @@ namespace vnotex
 
         bool isSessionEnabled() const;
 
+        virtual QString selectedText() const;
+
     public slots:
         virtual void handleEditorConfigChange() = 0;
 
@@ -226,8 +228,6 @@ namespace vnotex
 
         virtual void zoom(bool p_zoomIn) = 0;
 
-        virtual QString selectedText() const;
-
         void showZoomFactor(qreal p_factor);
 
         void showZoomDelta(int p_delta);

+ 2 - 0
src/widgets/widgets.pri

@@ -89,6 +89,7 @@ SOURCES += \
     $$PWD/tagexplorer.cpp \
     $$PWD/tagpopup.cpp \
     $$PWD/tagviewer.cpp \
+    $$PWD/terminalviewer.cpp \
     $$PWD/textviewwindow.cpp \
     $$PWD/toolbarhelper.cpp \
     $$PWD/treeview.cpp \
@@ -218,6 +219,7 @@ HEADERS += \
     $$PWD/tagexplorer.h \
     $$PWD/tagpopup.h \
     $$PWD/tagviewer.h \
+    $$PWD/terminalviewer.h \
     $$PWD/textviewwindow.h \
     $$PWD/textviewwindowhelper.h \
     $$PWD/toolbarhelper.h \

+ 29 - 0
tests/commonfull.pri

@@ -0,0 +1,29 @@
+include($$PWD/common.pri)
+
+QT += sql
+
+SRC_FOLDER = $$PWD/../src
+
+LIBS_FOLDER = $$PWD/../libs
+
+INCLUDEPATH *= $$SRC_FOLDER
+
+include($$LIBS_FOLDER/vtextedit/src/editor/editor_export.pri)
+
+include($$LIBS_FOLDER/vtextedit/src/libs/syntax-highlighting/syntax-highlighting_export.pri)
+
+include($$SRC_FOLDER/utils/utils.pri)
+
+include($$SRC_FOLDER/export/export.pri)
+
+include($$SRC_FOLDER/search/search.pri)
+
+include($$SRC_FOLDER/snippet/snippet.pri)
+
+include($$SRC_FOLDER/imagehost/imagehost.pri)
+
+include($$SRC_FOLDER/task/task.pri)
+
+include($$SRC_FOLDER/core/core.pri)
+
+include($$SRC_FOLDER/widgets/widgets.pri)

+ 1 - 22
tests/test_core/test_notebook/test_notebook.pro

@@ -1,29 +1,8 @@
-include($$PWD/../../common.pri)
-
-QT += sql
+include($$PWD/../../commonfull.pri)
 
 TARGET = test_notebook
 TEMPLATE = app
 
-SRC_FOLDER = $$PWD/../../../src
-CORE_FOLDER = $$SRC_FOLDER/core
-
-INCLUDEPATH *= $$SRC_FOLDER
-
-LIBS_FOLDER = $$PWD/../../../libs
-
-include($$LIBS_FOLDER/vtextedit/src/editor/editor_export.pri)
-
-include($$LIBS_FOLDER/vtextedit/src/libs/syntax-highlighting/syntax-highlighting_export.pri)
-
-include($$CORE_FOLDER/core.pri)
-include($$SRC_FOLDER/widgets/widgets.pri)
-include($$SRC_FOLDER/utils/utils.pri)
-include($$SRC_FOLDER/export/export.pri)
-include($$SRC_FOLDER/search/search.pri)
-include($$SRC_FOLDER/snippet/snippet.pri)
-include($$SRC_FOLDER/imagehost/imagehost.pri)
-
 SOURCES += \
     dummynode.cpp \
     dummynotebook.cpp \

+ 77 - 0
tests/test_task/test_task.cpp

@@ -0,0 +1,77 @@
+#include "test_task.h"
+
+#include <QDebug>
+#include <QProcessEnvironment>
+
+#include <task/taskvariablemgr.h>
+#include <task/task.h>
+#include <core/configmgr.h>
+#include <core/coreconfig.h>
+#include <core/sessionconfig.h>
+
+using namespace tests;
+
+using namespace vnotex;
+
+TestTask::TestTask(QObject *p_parent)
+    : QObject(p_parent)
+{
+}
+
+void TestTask::initTestCase()
+{
+    ConfigMgr::initForUnitTest();
+}
+
+void TestTask::TestTaskVariableMgr()
+{
+    TaskVariableMgr mgr(nullptr);
+    mgr.init();
+
+    mgr.overrideVariable("notebookFolder", [](const Task *, const QString &val) {
+        Q_ASSERT(val.isEmpty());
+        return "/home/vnotex/vnote";
+    });
+
+    mgr.overrideVariable("notebookFolderName", [](const Task *, const QString &val) {
+        Q_ASSERT(val.isEmpty());
+        return "vnote";
+    });
+
+    mgr.overrideVariable("magic", [](const Task *, const QString &val) {
+        if (val.isEmpty()) {
+            return QString();
+        } else {
+            return val;
+        }
+    });
+
+    auto task = createTask();
+
+    auto result = mgr.evaluate(task.data(), "start ${notebookFolder} end");
+    QCOMPARE(result, "start /home/vnotex/vnote end");
+
+    result = mgr.evaluate(task.data(), "start ${notebookFolder} mid ${notebookFolderName} end");
+    QCOMPARE(result, "start /home/vnotex/vnote mid vnote end");
+
+    result = mgr.evaluate(task.data(), "${magic:yyyy} ${magic:MM} ${magic:dd}");
+    QCOMPARE("yyyy MM dd", result);
+
+    {
+        const auto env = QProcessEnvironment::systemEnvironment();
+        result = mgr.evaluate(task.data(), "${env:PATH} ${env:QT_PATH} ${env:nonexist}");
+        QCOMPARE(result, QString("%1 %2 %3").arg(env.value("PATH"), env.value("QT_PATH"), env.value("nonexist")));
+    }
+
+    result = mgr.evaluate(task.data(), "${config:main.core.toolbar_icon_size} ${config:main.core.nonexists} ${config:session.core.system_title_bar}");
+    QCOMPARE(result, QString("%1  %2").arg(ConfigMgr::getInst().getCoreConfig().getToolBarIconSize())
+                                      .arg(ConfigMgr::getInst().getSessionConfig().getSystemTitleBarEnabled()));
+}
+
+QSharedPointer<vnotex::Task> TestTask::createTask() const
+{
+    return QSharedPointer<Task>(new Task("en_US", "dummy_file", nullptr, nullptr));
+}
+
+QTEST_MAIN(tests::TestTask)
+

+ 31 - 0
tests/test_task/test_task.h

@@ -0,0 +1,31 @@
+#ifndef TESTS_TASK_TEST_TASK_H
+#define TESTS_TASK_TEST_TASK_H
+
+#include <QtTest>
+#include <QSharedPointer>
+
+namespace vnotex
+{
+    class Task;
+}
+
+namespace tests
+{
+    class TestTask : public QObject
+    {
+        Q_OBJECT
+    public:
+        explicit TestTask(QObject *p_parent = nullptr);
+
+    private slots:
+        void initTestCase();
+
+        // Define test cases here per slot.
+        void TestTaskVariableMgr();
+
+    private:
+        QSharedPointer<vnotex::Task> createTask() const;
+    };
+} // ns tests
+
+#endif // TESTS_UTILS_TEST_UTILS_H

+ 10 - 0
tests/test_task/test_task.pro

@@ -0,0 +1,10 @@
+include($$PWD/../commonfull.pri)
+
+TARGET = test_task
+TEMPLATE = app
+
+SOURCES += \
+    test_task.cpp
+
+HEADERS += \
+    test_task.h

+ 64 - 0
tests/test_utils/test_utils.cpp

@@ -2,7 +2,9 @@
 
 #include <QDebug>
 #include <QTemporaryDir>
+#include <QJsonArray>
 
+#include <utils/utils.h>
 #include <utils/pathutils.h>
 #include <utils/fileutils.h>
 
@@ -10,6 +12,68 @@ using namespace tests;
 
 using namespace vnotex;
 
+TestUtils::TestUtils(QObject *p_parent)
+    : QObject(p_parent)
+{
+}
+
+void TestUtils::initTestCase()
+{
+    m_obj["a"] = "a";
+
+    {
+        QJsonObject objb;
+        objb["a"] = "ba";
+        objb["b"] = 2;
+
+        {
+            QJsonObject objbc;
+            objbc["a"] = "bca";
+
+            objb["c"] = objbc;
+        }
+
+        m_obj["b"] = objb;
+    }
+
+    {
+        QJsonObject objc;
+        objc["a"] = "ca";
+
+        QJsonArray arr;
+        arr.append("cb0");
+        arr.append("cb1");
+        arr.append("cb2");
+        objc["b"] = arr;
+
+        m_obj["c"] = objc;
+    }
+}
+
+void TestUtils::testParseAndReadJson_data()
+{
+    QTest::addColumn<QString>("exp");
+    QTest::addColumn<QJsonValue>("result");
+
+    QTest::newRow("empty") << "" << QJsonValue(m_obj);
+    QTest::newRow("a") << "a" << QJsonValue("a");
+    QTest::newRow("ba") << "b.a" << QJsonValue("ba");
+    QTest::newRow("bb") << "b.b" << QJsonValue(2);
+    QTest::newRow("bca") << "b.c.a" << QJsonValue("bca");
+    QTest::newRow("ca") << "c.a" << QJsonValue("ca");
+    QTest::newRow("cb0") << "c.b[0]" << QJsonValue("cb0");
+    QTest::newRow("cb1") << "c.b[1]" << QJsonValue("cb1");
+    QTest::newRow("cb2") << "c.b[2]" << QJsonValue("cb2");
+}
+
+void TestUtils::testParseAndReadJson()
+{
+    QFETCH(QString, exp);
+    QFETCH(QJsonValue, result);
+
+    QCOMPARE(Utils::parseAndReadJson(m_obj, exp), result);
+}
+
 void TestUtils::testParentDirPath_data()
 {
     QTest::addColumn<QString>("path");

+ 12 - 0
tests/test_utils/test_utils.h

@@ -2,16 +2,25 @@
 #define TESTS_UTILS_TEST_UTILS_H
 
 #include <QtTest>
+#include <QJsonObject>
 
 namespace tests
 {
     class TestUtils : public QObject
     {
         Q_OBJECT
+    public:
+        explicit TestUtils(QObject *p_parent = nullptr);
 
     private slots:
+        void initTestCase();
+
         // Define test cases here per slot.
 
+        // Utils tests.
+        void testParseAndReadJson_data();
+        void testParseAndReadJson();
+
         // PathUtils Tests.
         void testParentDirPath_data();
         void testParentDirPath();
@@ -32,6 +41,9 @@ namespace tests
         void testRenameFile();
 
         void testIsText();
+
+    private:
+        QJsonObject m_obj;
     };
 } // ns tests
 

+ 2 - 0
tests/test_utils/test_utils.pro

@@ -10,10 +10,12 @@ INCLUDEPATH *= $$SRC_FOLDER
 
 SOURCES += \
     test_utils.cpp \
+    $$UTILS_FOLDER/utils.cpp \
     $$UTILS_FOLDER/pathutils.cpp \
     $$UTILS_FOLDER/fileutils.cpp
 
 HEADERS += \
     test_utils.h \
+    $$UTILS_FOLDER/utils.h \
     $$UTILS_FOLDER/pathutils.h \
     $$UTILS_FOLDER/fileutils.h

+ 2 - 1
tests/tests.pro

@@ -2,4 +2,5 @@ TEMPLATE = subdirs
 
 SUBDIRS = \
     test_utils \
-    test_core
+    test_core \
+    test_task

Some files were not shown because too many files changed in this diff