فهرست منبع

Feature Task System (#1947)

* add task system code

* enable ci build

* Revert "enable ci build"

This reverts commit 8c457a22e44e64c7d87804fc3c76ee778c1c3b6f.
tootal 3 سال پیش
والد
کامیت
911392deab
39فایلهای تغییر یافته به همراه2194 افزوده شده و 8 حذف شده
  1. 18 0
      src/core/configmgr.cpp
  2. 4 0
      src/core/configmgr.h
  3. 10 0
      src/core/core.pri
  4. 102 0
      src/core/shellexecution.cpp
  5. 42 0
      src/core/shellexecution.h
  6. 557 0
      src/core/task.cpp
  7. 185 0
      src/core/task.h
  8. 233 0
      src/core/taskhelper.cpp
  9. 50 0
      src/core/taskhelper.h
  10. 192 0
      src/core/taskmgr.cpp
  11. 79 0
      src/core/taskmgr.h
  12. 334 0
      src/core/taskvariablemgr.cpp
  13. 73 0
      src/core/taskvariablemgr.h
  14. 14 0
      src/core/vnotex.cpp
  15. 11 0
      src/core/vnotex.h
  16. 1 0
      src/data/core/core.qrc
  17. 1 0
      src/data/core/icons/task_menu.svg
  18. 8 0
      src/data/extra/extra.qrc
  19. 1 0
      src/data/extra/tasks/git/commit.svg
  20. 80 0
      src/data/extra/tasks/git/git.json
  21. 1 0
      src/data/extra/tasks/git/git.svg
  22. 1 0
      src/data/extra/tasks/git/history.svg
  23. 1 0
      src/data/extra/tasks/git/initialization.svg
  24. 1 0
      src/data/extra/tasks/git/pull.svg
  25. 0 0
      src/data/extra/tasks/git/push.svg
  26. 1 0
      src/data/extra/tasks/git/status.svg
  27. 19 0
      src/utils/fileutils.cpp
  28. 7 0
      src/utils/fileutils.h
  29. 2 2
      src/utils/iconutils.cpp
  30. 18 1
      src/widgets/dialogs/selectdialog.cpp
  31. 8 1
      src/widgets/dialogs/selectdialog.h
  32. 16 0
      src/widgets/dockwidgethelper.cpp
  33. 3 0
      src/widgets/dockwidgethelper.h
  34. 27 0
      src/widgets/mainwindow.cpp
  35. 7 0
      src/widgets/mainwindow.h
  36. 2 2
      src/widgets/markdownviewwindow.h
  37. 2 2
      src/widgets/textviewwindow.h
  38. 76 0
      src/widgets/toolbarhelper.cpp
  39. 7 0
      src/widgets/toolbarhelper.h

+ 18 - 0
src/core/configmgr.cpp

@@ -227,6 +227,12 @@ bool ConfigMgr::checkAppConfig()
     FileUtils::copyDir(extraDataRoot + QStringLiteral("/themes"),
                        appConfigDir.filePath(QStringLiteral("themes")));
 
+    // Copy tasks.
+    qApp->processEvents();
+    splash->showMessage("Copying tasks");
+    FileUtils::copyDir(extraDataRoot + QStringLiteral("/tasks"),
+                       appConfigDir.filePath(QStringLiteral("tasks")));
+
     // Copy docs.
     qApp->processEvents();
     splash->showMessage("Copying docs");
@@ -377,6 +383,18 @@ QString ConfigMgr::getUserThemeFolder() const
     return folderPath;
 }
 
+QString ConfigMgr::getAppTaskFolder() const
+{
+    return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("tasks"));
+}
+
+QString ConfigMgr::getUserTaskFolder() const
+{
+    auto folderPath = PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("tasks"));
+    QDir().mkpath(folderPath);
+    return folderPath;
+}
+
 QString ConfigMgr::getAppWebStylesFolder() const
 {
     return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("web-styles"));

+ 4 - 0
src/core/configmgr.h

@@ -74,6 +74,10 @@ namespace vnotex
 
         QString getUserThemeFolder() const;
 
+        QString getAppTaskFolder() const;
+
+        QString getUserTaskFolder() const;
+
         QString getAppWebStylesFolder() const;
 
         QString getUserWebStylesFolder() const;

+ 10 - 0
src/core/core.pri

@@ -29,6 +29,11 @@ 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 \
@@ -60,6 +65,11 @@ 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 \

+ 102 - 0
src/core/shellexecution.cpp

@@ -0,0 +1,102 @@
+#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;
+}

+ 42 - 0
src/core/shellexecution.h

@@ -0,0 +1,42 @@
+#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

+ 557 - 0
src/core/task.cpp

@@ -0,0 +1,557 @@
+#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;
+}

+ 185 - 0
src/core/task.h

@@ -0,0 +1,185 @@
+#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

+ 233 - 0
src/core/taskhelper.cpp

@@ -0,0 +1,233 @@
+#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()
+{
+    
+}

+ 50 - 0
src/core/taskhelper.h

@@ -0,0 +1,50 @@
+#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

+ 192 - 0
src/core/taskmgr.cpp

@@ -0,0 +1,192 @@
+#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);
+}

+ 79 - 0
src/core/taskmgr.h

@@ -0,0 +1,79 @@
+#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

+ 334 - 0
src/core/taskvariablemgr.cpp

@@ -0,0 +1,334 @@
+#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);
+}
+

+ 73 - 0
src/core/taskvariablemgr.h

@@ -0,0 +1,73 @@
+#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

+ 14 - 0
src/core/vnotex.cpp

@@ -27,6 +27,8 @@ VNoteX::VNoteX(QObject *p_parent)
 
     initThemeMgr();
 
+    initTaskMgr();
+
     initNotebookMgr();
 
     initBufferMgr();
@@ -40,6 +42,7 @@ void VNoteX::initLoad()
 {
     qDebug() << "start init which may take a while";
     m_notebookMgr->loadNotebooks();
+    m_taskMgr->init();
 }
 
 void VNoteX::initThemeMgr()
@@ -56,11 +59,22 @@ void VNoteX::initThemeMgr()
     m_themeMgr = new ThemeMgr(configMgr.getCoreConfig().getTheme(), this);
 }
 
+void VNoteX::initTaskMgr()
+{
+    Q_ASSERT(!m_taskMgr);
+    m_taskMgr = new TaskMgr(this);
+}
+
 ThemeMgr &VNoteX::getThemeMgr() const
 {
     return *m_themeMgr;
 }
 
+TaskMgr &VNoteX::getTaskMgr() const
+{
+    return *m_taskMgr;
+}
+
 void VNoteX::setMainWindow(MainWindow *p_mainWindow)
 {
     Q_ASSERT(!m_mainWindow);

+ 11 - 0
src/core/vnotex.h

@@ -6,6 +6,7 @@
 
 #include "noncopyable.h"
 #include "thememgr.h"
+#include "taskmgr.h"
 #include "global.h"
 
 namespace vnotex
@@ -35,6 +36,8 @@ namespace vnotex
 
         ThemeMgr &getThemeMgr() const;
 
+        TaskMgr &getTaskMgr() const;
+
         void setMainWindow(MainWindow *p_mainWindow);
         MainWindow *getMainWindow() const;
 
@@ -79,6 +82,9 @@ namespace vnotex
         // Requested to new a folder in current notebook.
         void newFolderRequested();
 
+        // Requested to show output message.
+        void showOutputRequested(const QString &p_text);
+
         // Requested to show status message.
         void statusMessageRequested(const QString &p_message, int p_timeoutMilliseconds);
 
@@ -116,6 +122,8 @@ namespace vnotex
 
         void initThemeMgr();
 
+        void initTaskMgr();
+
         void initNotebookMgr();
 
         void initBufferMgr();
@@ -129,6 +137,9 @@ namespace vnotex
         // QObject managed.
         ThemeMgr *m_themeMgr;
 
+        // QObject managed.
+        TaskMgr *m_taskMgr;
+
         // QObject managed.
         NotebookMgr *m_notebookMgr;
 

+ 1 - 0
src/data/core/core.qrc

@@ -8,6 +8,7 @@
         <file>icons/import_notebook_of_vnote2.svg</file>
         <file>icons/new_notebook.svg</file>
         <file>icons/notebook_menu.svg</file>
+        <file>icons/task_menu.svg</file>
         <file>icons/advanced_settings.svg</file>
         <file>icons/new_notebook_from_folder.svg</file>
         <file>icons/discard_editor.svg</file>

+ 1 - 0
src/data/core/icons/task_menu.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1611462360764" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2562" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M585.142857 804.571429h365.714286v-73.142858H585.142857v73.142858z m-219.428571-292.571429h585.142857v-73.142857H365.714286v73.142857z m365.714285-292.571429h219.428572V146.285714h-219.428572v73.142857z m292.571429 475.428572v146.285714c0 20.004571-16.566857 36.571429-36.571429 36.571429H36.571429c-20.004571 0-36.571429-16.566857-36.571429-36.571429v-146.285714c0-20.004571 16.566857-36.571429 36.571429-36.571429h950.857142c20.004571 0 36.571429 16.566857 36.571429 36.571429z m0-292.571429v146.285715c0 20.004571-16.566857 36.571429-36.571429 36.571428H36.571429c-20.004571 0-36.571429-16.566857-36.571429-36.571428v-146.285715c0-20.004571 16.566857-36.571429 36.571429-36.571428h950.857142c20.004571 0 36.571429 16.566857 36.571429 36.571428z m0-292.571428v146.285714c0 20.004571-16.566857 36.571429-36.571429 36.571429H36.571429c-20.004571 0-36.571429-16.566857-36.571429-36.571429V109.714286c0-20.004571 16.566857-36.571429 36.571429-36.571429h950.857142c20.004571 0 36.571429 16.566857 36.571429 36.571429z" fill="" p-id="2563"></path></svg>

+ 8 - 0
src/data/extra/extra.qrc

@@ -150,6 +150,14 @@
         <file>themes/pure/up.svg</file>
         <file>themes/pure/up_disabled.svg</file>
         <file>themes/pure/web.css</file>
+        <file>tasks/git/git.json</file>
+        <file>tasks/git/git.svg</file>
+        <file>tasks/git/commit.svg</file>
+        <file>tasks/git/history.svg</file>
+        <file>tasks/git/initialization.svg</file>
+        <file>tasks/git/pull.svg</file>
+        <file>tasks/git/push.svg</file>
+        <file>tasks/git/status.svg</file>
         <file>syntax-highlighting/themes/markdown-default.theme</file>
         <file>syntax-highlighting/themes/markdown-breeze-dark.theme</file>
         <file>syntax-highlighting/themes/default.theme</file>

+ 1 - 0
src/data/extra/tasks/git/commit.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1612349138300" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4255" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M960 576H692.224c-26.432 74.432-96.704 128-180.224 128-83.456 0-153.792-53.568-180.224-128H64a64 64 0 1 1 0-128h267.776c26.432-74.368 96.768-128 180.224-128 83.52 0 153.792 53.632 180.224 128H960c35.392 0 64 28.608 64 64s-28.608 64-64 64z m-448-128a64 64 0 1 0 0 128c35.392 0 64-28.608 64-64s-28.608-64-64-64z" fill="" p-id="4256"></path></svg>

+ 80 - 0
src/data/extra/tasks/git/git.json

@@ -0,0 +1,80 @@
+{
+    "version": "0.1.4",
+    "label": "Git",
+    "icon": "git.svg",
+    "tasks": [
+        {
+            "label": {
+                "en_US": "Initialize",
+                "zh_CN": "初始化",
+                "ja_JP": "イニシャライズ"
+            },
+            "icon": "initialization.svg",
+            "command": "git init -b main"
+        },
+        {
+            "label": {
+                "en_US": "Status",
+                "zh_CN": "状态",
+                "ja_JP": "ステータス"
+            },
+            "icon": "status.svg",
+            "command": "git status"
+        },
+        {
+            "label": {
+                "en_US": "Commit",
+                "zh_CN": "提交",
+                "ja_JP": "全てコミット"
+            },
+            "icon": "commit.svg",
+            "windows" :{
+                "command": "git add -A -- . ; if ($?) { git commit --message=\"${input:msg}\" }"
+            },
+            "command": "git add -A -- . && git commit --message=\"${input:msg}\"",
+            "inputs": [
+                {
+                    "id": "msg",
+                    "type": "promptString",
+                    "description": {
+                        "en_US": "Please provide a commit message",
+                        "zh_CN": "请输入提交信息",
+                        "ja_JP": "コミットメッセージを提供してください"
+                    },
+                    "default": {
+                        "en_US": "Update note on ${magic:datetime}",
+                        "zh_CN": "更新笔记于 ${magic:datetime}",
+                        "ja_JP": "アップデート ${magic:datetime}"
+                    }
+                }
+            ]
+        },
+        {
+            "label": {
+                "en_US": "Push",
+                "zh_CN": "上传",
+                "ja_JP": "プッシュ"
+            },
+            "icon": "push.svg",
+            "command": "git push"
+        },
+        {
+            "label": {
+                "en_US": "Pull",
+                "zh_CN": "下载",
+                "ja_JP": "プル"
+            },
+            "icon": "pull.svg",
+            "command": "git pull --no-rebase"
+        },
+        {
+            "label": {
+                "en_US": "Log",
+                "zh_CN": "日志",
+                "ja_JP": "ログ"
+            },
+            "icon": "history.svg",
+            "command": "git log -10 --graph --pretty=format:'%h -%d %s (%cr) <%an>' --abbrev-commit"
+        }
+    ]
+}

+ 1 - 0
src/data/extra/tasks/git/git.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1612348787730" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2148" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1004.628512 466.345889L557.525257 19.285301a66.133223 66.133223 0 0 0-93.354511 0L371.541567 112.085147l117.759804 117.759803a78.421203 78.421203 0 0 1 99.285168 99.8825l113.407811 113.493145a78.421203 78.421203 0 0 1 81.066532 129.663784 78.378536 78.378536 0 0 1-110.933149 0 78.762535 78.762535 0 0 1-17.237304-85.162525L548.693272 382.079363v278.399536c7.509321 3.669327 14.591976 8.661319 20.821299 14.847975a78.847869 78.847869 0 0 1 0 110.933149 78.677202 78.677202 0 0 1-111.317148 0 78.250536 78.250536 0 0 1 0-110.847815c7.76532-7.679987 16.511972-13.482644 25.81329-17.322638V376.959372a78.250536 78.250536 0 0 1-42.495929-102.826496L325.802977 157.866404 19.200821 464.255226c-25.599957 25.81329-25.599957 67.583887 0 93.397178l447.145922 447.017922a65.91989 65.91989 0 0 0 93.269178 0l445.012591-445.012592a65.877224 65.877224 0 0 0 0-93.311845" fill="" p-id="2149"></path></svg>

+ 1 - 0
src/data/extra/tasks/git/history.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1612348938376" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2143" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M554.453333 128c-212.266667 0-383.786667 171.946667-383.786667 384l-128 0 166.186667 166.186667 2.986667 6.186667 172.16-172.373333-128 0c0-164.906667 133.76-298.666667 298.666667-298.666667s298.666667 133.76 298.666667 298.666667-133.76 298.666667-298.666667 298.666667c-82.56 0-157.013333-33.706667-210.986667-87.68l-60.373333 60.373333c69.333333 69.546667 165.12 112.64 271.146667 112.64 212.266667 0 384.213333-171.946667 384.213333-384s-171.946667-384-384.213333-384zM512 341.333333l0 213.333333 182.613333 108.373333 30.72-51.84-149.333333-88.533333 0-181.333333-64 0z" fill="" p-id="2144"></path></svg>

+ 1 - 0
src/data/extra/tasks/git/initialization.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1612619426558" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2127" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M885.333333 917.333333h-746.666666c-17.066667 0-32-14.933333-32-32S121.6 853.333333 138.666667 853.333333h746.666666c17.066667 0 32 14.933333 32 32s-14.933333 32-32 32zM546.133333 785.066667c-64 0-128-19.2-183.466666-53.333334-14.933333-8.533333-19.2-29.866667-10.666667-44.8 8.533333-14.933333 29.866667-19.2 44.8-10.666666 64 40.533333 140.8 53.333333 215.466667 36.266666 149.333333-36.266667 241.066667-185.6 204.8-334.933333-17.066667-72.533333-61.866667-132.266667-125.866667-170.666667-64-38.4-138.666667-49.066667-209.066667-32-89.6 21.333333-168.533333 96-200.533333 187.733334-6.4 17.066667-23.466667 25.6-40.533333 19.2-17.066667-6.4-25.6-23.466667-19.2-40.533334 38.4-110.933333 134.4-202.666667 247.466666-228.266666 89.6-21.333333 179.2-6.4 258.133334 40.533333 78.933333 46.933333 132.266667 121.6 153.6 211.2 44.8 183.466667-68.266667 366.933333-251.733334 411.733333-27.733333 6.4-55.466667 8.533333-83.2 8.533334z" fill="" p-id="2128"></path><path d="M230.4 437.333333c-29.866667 0-57.6-21.333333-64-51.2l-29.866667-123.733333c-4.266667-17.066667 6.4-34.133333 23.466667-38.4 17.066667-4.266667 34.133333 6.4 38.4 23.466667l29.866667 123.733333c0 2.133333 2.133333 2.133333 4.266666 2.133333l123.733334-29.866666c17.066667-4.266667 34.133333 6.4 38.4 23.466666 4.266667 17.066667-6.4 34.133333-23.466667 38.4l-123.733333 29.866667c-6.4 0-10.666667 2.133333-17.066667 2.133333z" fill="" p-id="2129"></path></svg>

+ 1 - 0
src/data/extra/tasks/git/pull.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1612349304217" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5782" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M980.76306 840.106926c0 101.570274-82.384097 183.893074-183.893073 183.893074s-183.893074-82.322799-183.893074-183.893074c0-79.993487 51.367465-147.298352 122.595382-172.614298V349.725396c0-33.897623-27.400068-61.297691-61.297691-61.297691v61.236393a61.338556 61.338556 0 0 1-104.635159 43.398766l-122.595382-122.656681A61.032068 61.032068 0 0 1 429.083839 227.068716c0-15.630911 6.007174-31.32312 17.960224-43.27617l122.595382-122.595383A61.256826 61.256826 0 1 1 674.274604 104.473333V165.832322c101.508977 0 183.893074 82.322799 183.893074 183.893074v317.767232c71.227917 25.315946 122.595383 92.620812 122.595382 172.614298z m-183.893073-61.297691c-33.897623 0-61.297691 27.400068-61.297692 61.297691s27.400068 61.297691 61.297692 61.297691 61.297691-27.400068 61.297691-61.297691-27.400068-61.297691-61.297691-61.297691z m-429.083839 61.297691c0 101.570274-82.322799 183.893074-183.893074 183.893074s-183.893074-82.322799-183.893074-183.893074c0-79.993487 51.306168-147.298352 122.595383-172.614298V399.744312C51.306168 374.428366 0 307.1235 0 227.130013c0-101.570274 82.322799-183.893074 183.893074-183.893073s183.893074 82.322799 183.893074 183.893073c0 79.993487-51.306168 147.298352-122.595383 172.614299v267.687018c71.289215 25.377244 122.595383 92.682109 122.595383 172.675596zM183.893074 165.832322a61.297691 61.297691 0 1 0 0 122.595383 61.297691 61.297691 0 1 0 0-122.595383z m0 612.976913a61.297691 61.297691 0 1 0 0 122.595382 61.297691 61.297691 0 1 0 0-122.595382z" fill="" p-id="5783"></path></svg>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
src/data/extra/tasks/git/push.svg


+ 1 - 0
src/data/extra/tasks/git/status.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1612619602951" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3508" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M849.938174 262.216074c-61.665455 0-111.670336 49.713238-111.670336 111.01235 0 21.939685 6.40385 42.389436 17.449417 59.607586l-76.68452 76.68452c-22.715351-15.924691-50.424436-25.28078-80.340793-25.28078-34.92851 0-66.858734 12.749372-91.339288 33.821293l-34.43016-34.429137c7.966439-14.18814 12.512988-30.531364 12.512988-47.933709 0-54.42147-44.395116-98.547457-99.132788-98.547457s-99.109252 44.12701-99.109252 98.547457c0 12.93152 2.51017 25.278733 7.064906 36.592406l-58.317197 58.30901c-17.715477-11.746532-38.993083-18.600637-61.881372-18.600637-61.688991 0-111.670336 49.714262-111.670336 111.01235 0 61.324694 49.981345 111.01235 111.670336 111.01235 61.665455 0 111.671359-49.688679 111.671359-111.01235 0-18.445094-4.533246-35.837206-12.543687-51.145867l55.787584-55.787584c16.182564 11.426237 35.96512 18.144242 57.32766 18.144242 16.989953 0 32.981159-4.251837 46.960545-11.739369l39.85266 39.847543c-8.980535 18.331507-14.024411 38.920428-14.024411 60.682058 0 76.641541 62.494333 138.771577 139.600455 138.771577S738.267838 699.653891 738.267838 623.01235c0-26.885324-7.696286-51.980885-21.00643-73.240071l79.122035-79.110779c15.901155 8.655124 34.148751 13.579273 53.55473 13.579273 61.690014 0 111.671359-49.688679 111.671359-111.011326C961.609533 311.929312 911.628188 262.216074 849.938174 262.216074z" fill="" p-id="3509"></path></svg>

+ 19 - 0
src/utils/fileutils.cpp

@@ -357,3 +357,22 @@ void FileUtils::removeEmptyDir(const QString &p_dirPath)
         removeDirIfEmpty(childPath);
     }
 }
+
+QStringList FileUtils::entryListRecursively(const QString &p_dirPath,
+                                            const QStringList &p_nameFilters,
+                                            QDir::Filters p_filters)
+{
+    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));
+    }
+    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));
+    }
+    return entrys;
+}

+ 7 - 0
src/utils/fileutils.h

@@ -6,6 +6,7 @@
 #include <QImage>
 #include <QPixmap>
 #include <QJsonObject>
+#include <QDir>
 
 class QTemporaryFile;
 
@@ -77,6 +78,12 @@ namespace vnotex
         // Go through @p_dirPath recursively and delete all empty dirs.
         // @p_dirPath itself is not deleted.
         static void removeEmptyDir(const QString &p_dirPath);
+
+        // Go through @p_dirPath recursively and get all entrys.
+        // @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);
     };
 } // ns vnotex
 

+ 2 - 2
src/utils/iconutils.cpp

@@ -59,8 +59,8 @@ QString IconUtils::replaceForegroundOfIcon(const QString &p_iconContent, const Q
         return p_iconContent;
     }
 
-    // Must have a # to avoid fill="none".
-    QRegExp styleRe("(\\s|\"|;)(fill|stroke)(:|(=\"))#[^#\"\\s]+");
+    // Negative lookahead to avoid fill="none".
+    QRegExp styleRe(R"((\s|"|;)(fill|stroke)(:|(="))(?!none)[^;"]*)");
     if (p_iconContent.indexOf(styleRe) > -1) {
         auto newContent(p_iconContent);
         newContent.replace(styleRe, QString("\\1\\2\\3%1").arg(p_foreground));

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

@@ -22,11 +22,28 @@ SelectDialog::SelectDialog(const QString &p_title, QWidget *p_parent)
     setupUI(p_title);
 }
 
-void SelectDialog::setupUI(const QString &p_title)
+SelectDialog::SelectDialog(const QString &p_title,
+                           const QString &p_text,
+                           QWidget *p_parent)
+    : QDialog(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, p_text);
+}
+
+void SelectDialog::setupUI(const QString &p_title, const QString &p_text)
 {
     auto mainLayout = new QVBoxLayout(this);
     mainLayout->setContentsMargins(0, 0, 0, 0);
 
+    if (!p_text.isNull()) {
+        m_label = new QLabel(p_text, this);
+        mainLayout->addWidget(m_label);
+    }
+
     m_list = new QListWidget(this);
     m_list->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
     m_list->setSelectionMode(QAbstractItemView::SingleSelection);

+ 8 - 1
src/widgets/dialogs/selectdialog.h

@@ -10,6 +10,7 @@ class QListWidget;
 class QListWidgetItem;
 class QShowEvent;
 class QKeyEvent;
+class QLabel;
 
 namespace vnotex
 {
@@ -19,6 +20,10 @@ namespace vnotex
     public:
         SelectDialog(const QString &p_title, QWidget *p_parent = nullptr);
 
+        SelectDialog(const QString &p_title,
+                     const QString &p_text,
+                     QWidget *p_parent = nullptr);
+
         // @p_selectID should >= 0.
         void addSelection(const QString &p_selectStr, int p_selectID);
 
@@ -37,12 +42,14 @@ namespace vnotex
     private:
         enum { CANCEL_ID = -1 };
 
-        void setupUI(const QString &p_title);
+        void setupUI(const QString &p_title, const QString &p_text = QString());
 
         void updateSize();
 
         int m_choice = CANCEL_ID;
 
+        QLabel *m_label = nullptr;
+
         QListWidget *m_list = nullptr;
 
         QMap<QChar, QListWidgetItem *> m_shortcuts;

+ 16 - 0
src/widgets/dockwidgethelper.cpp

@@ -6,6 +6,7 @@
 #include <QHelpEvent>
 #include <QToolTip>
 #include <QShortcut>
+#include <QTextEdit>
 
 #include <core/vnotex.h>
 #include <core/thememgr.h>
@@ -112,6 +113,8 @@ void DockWidgetHelper::setupDocks()
 
     setupOutlineDock();
 
+    setupOutputDock();
+
     setupLocationListDock();
 
     setupShortcuts();
@@ -145,6 +148,19 @@ void DockWidgetHelper::setupOutlineDock()
     m_mainWindow->addDockWidget(Qt::RightDockWidgetArea, dock);
 }
 
+void DockWidgetHelper::setupOutputDock()
+{
+    auto dock = createDockWidget(DockIndex::OutputDock, tr("Output"), m_mainWindow);
+
+    dock->setObjectName(QStringLiteral("OutputDock.vnotex"));
+    dock->setAllowedAreas(Qt::BottomDockWidgetArea);
+
+    dock->setWidget(m_mainWindow->m_outputViewer);
+    dock->setFocusProxy(m_mainWindow->m_outputViewer);
+    dock->hide();
+    m_mainWindow->addDockWidget(Qt::BottomDockWidgetArea, dock);
+}
+
 void DockWidgetHelper::setupSearchDock()
 {
     auto dock = createDockWidget(DockIndex::SearchDock, tr("Search"), m_mainWindow);

+ 3 - 0
src/widgets/dockwidgethelper.h

@@ -29,6 +29,7 @@ namespace vnotex
             SearchDock,
             SnippetDock,
             OutlineDock,
+            OutputDock,
             LocationListDock,
             MaxDock
         };
@@ -97,6 +98,8 @@ namespace vnotex
 
         void setupOutlineDock();
 
+        void setupOutputDock();
+
         void setupSearchDock();
 
         void setupSnippetDock();

+ 27 - 0
src/widgets/mainwindow.cpp

@@ -257,6 +257,8 @@ void MainWindow::setupDocks()
 
     setupOutlineViewer();
 
+    setupOutputViewer();
+
     setupHistoryPanel();
 
     setupSearchPanel();
@@ -512,11 +514,36 @@ void MainWindow::setupOutlineViewer()
             this, &MainWindow::focusViewArea);
 }
 
+void MainWindow::setupOutputViewer()
+{
+    m_outputViewer = new QTextEdit(this);
+    m_outputViewer->setObjectName("OutputViewer.vnotex");
+    m_outputViewer->setReadOnly(true);
+
+    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();
+    });
+}
+
 const QVector<QDockWidget *> &MainWindow::getDocks() const
 {
     return m_dockWidgetHelper.getDocks();
 }
 
+ViewArea *MainWindow::getViewArea() const
+{
+    return m_viewArea;
+}
+
 void MainWindow::focusViewArea()
 {
     m_viewArea->focus();

+ 7 - 0
src/widgets/mainwindow.h

@@ -13,6 +13,7 @@ class QDockWidget;
 class QSystemTrayIcon;
 class QTimer;
 class QLabel;
+class QTextEdit;
 
 namespace vnotex
 {
@@ -49,6 +50,8 @@ namespace vnotex
 
         const QVector<QDockWidget *> &getDocks() const;
 
+        ViewArea *getViewArea() const;
+
         void setContentAreaExpanded(bool p_expanded);
         // Should be called after MainWindow is shown.
         bool isContentAreaExpanded() const;
@@ -103,6 +106,8 @@ namespace vnotex
 
         void setupOutlineViewer();
 
+        void setupOutputViewer();
+
         void setupSearchPanel();
 
         void setupLocationList();
@@ -162,6 +167,8 @@ namespace vnotex
 
         OutlineViewer *m_outlineViewer = nullptr;
 
+        QTextEdit *m_outputViewer = nullptr;
+
         LocationList *m_locationList = nullptr;
 
         SearchPanel *m_searchPanel = nullptr;

+ 2 - 2
src/widgets/markdownviewwindow.h

@@ -42,6 +42,8 @@ namespace vnotex
 
         QString getLatestContent() const Q_DECL_OVERRIDE;
 
+        QString selectedText() const Q_DECL_OVERRIDE;
+
         void setMode(ViewWindowMode p_mode) Q_DECL_OVERRIDE;
 
         QSharedPointer<OutlineProvider> getOutlineProvider() Q_DECL_OVERRIDE;
@@ -101,8 +103,6 @@ namespace vnotex
 
         QPoint getFloatingWidgetPosition() Q_DECL_OVERRIDE;
 
-        QString selectedText() const Q_DECL_OVERRIDE;
-
         void updateViewModeMenu(QMenu *p_menu) Q_DECL_OVERRIDE;
 
     private:

+ 2 - 2
src/widgets/textviewwindow.h

@@ -25,6 +25,8 @@ namespace vnotex
 
         QString getLatestContent() const Q_DECL_OVERRIDE;
 
+        QString selectedText() const Q_DECL_OVERRIDE;
+
         void setMode(ViewWindowMode p_mode) Q_DECL_OVERRIDE;
 
         void openTwice(const QSharedPointer<FileOpenParameters> &p_paras) Q_DECL_OVERRIDE;
@@ -68,8 +70,6 @@ namespace vnotex
 
         QPoint getFloatingWidgetPosition() Q_DECL_OVERRIDE;
 
-        QString selectedText() const Q_DECL_OVERRIDE;
-
     private:
         void setupUI();
 

+ 76 - 0
src/widgets/toolbarhelper.cpp

@@ -26,6 +26,7 @@
 #include <core/markdowneditorconfig.h>
 #include <core/fileopenparameters.h>
 #include <core/htmltemplatehelper.h>
+#include <core/exception.h>
 #include "propertydefs.h"
 #include "dialogs/settings/settingsdialog.h"
 #include "dialogs/updater.h"
@@ -282,6 +283,78 @@ QToolBar *ToolBarHelper::setupQuickAccessToolBar(MainWindow *p_win, QToolBar *p_
     return tb;
 }
 
+void ToolBarHelper::setupTaskMenu(QMenu *p_menu)
+{
+    p_menu->clear();
+    const auto &taskMgr = VNoteX::getInst().getTaskMgr();
+    for (auto task : taskMgr.getAppTasks()) {
+        addTaskMenu(p_menu, task);
+    }
+    p_menu->addSeparator();
+    for (auto task : taskMgr.getUserTasks()) {
+        addTaskMenu(p_menu, task);
+    }
+    p_menu->addSeparator();
+    for (auto task : taskMgr.getNotebookTasks()) {
+        addTaskMenu(p_menu, task);
+    }
+}
+
+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();
+    auto label = p_task->getLabel();
+    label = label.replace("&", "&&");
+    QIcon icon;
+    try {
+        auto taskIcon = p_task->getIcon();
+        if (!taskIcon.isEmpty()) {
+            icon = generateIcon(p_task->getIcon());
+        }
+    }  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);
+        }
+        action = menu->menuAction();
+    }
+    action->setIcon(icon);
+    WidgetUtils::addActionShortcut(action, p_task->getShortcut());
+    MainWindow::connect(action, &QAction::triggered,
+                        p_task, &Task::run);
+}
+
+QToolBar *ToolBarHelper::setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar)
+{
+    auto tb = p_toolBar;
+    if (!tb) {
+        tb = createToolBar(p_win, MainWindow::tr("Task"), "TaskToolBar");
+    }
+
+    auto act = tb->addAction(generateIcon("task_menu.svg"), MainWindow::tr("Task"));
+    auto btn = dynamic_cast<QToolButton *>(tb->widgetForAction(act));
+    btn->setPopupMode(QToolButton::InstantPopup);
+    btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true);
+
+    auto taskMenu = WidgetsFactory::createMenu(tb);
+    MainWindow::connect(&VNoteX::getInst().getTaskMgr(), &TaskMgr::taskChanged,
+                        taskMenu, [taskMenu]() {
+        setupTaskMenu(taskMenu);
+    });
+    btn->setMenu(taskMenu);
+    return tb;
+}
+
 QToolBar *ToolBarHelper::setupSettingsToolBar(MainWindow *p_win, QToolBar *p_toolBar)
 {
     auto tb = p_toolBar;
@@ -344,6 +417,8 @@ void ToolBarHelper::setupToolBars(MainWindow *p_mainWindow)
 
     setupQuickAccessToolBar(p_mainWindow, nullptr);
 
+    setupTaskToolBar(p_mainWindow, nullptr);
+
     setupSettingsToolBar(p_mainWindow, nullptr);
 }
 
@@ -355,6 +430,7 @@ void ToolBarHelper::setupToolBars(MainWindow *p_mainWindow, QToolBar *p_toolBar)
 
     setupFileToolBar(p_mainWindow, p_toolBar);
     setupQuickAccessToolBar(p_mainWindow, p_toolBar);
+    setupTaskToolBar(p_mainWindow, p_toolBar);
     setupSettingsToolBar(p_mainWindow, p_toolBar);
 }
 

+ 7 - 0
src/widgets/toolbarhelper.h

@@ -9,6 +9,7 @@ class QMenu;
 namespace vnotex
 {
     class MainWindow;
+    class Task;
 
     // Tool bar helper for MainWindow.
     class ToolBarHelper
@@ -33,6 +34,12 @@ namespace vnotex
 
         static QToolBar *setupQuickAccessToolBar(MainWindow *p_win, QToolBar *p_toolBar);
 
+        static void setupTaskMenu(QMenu *p_menu);
+
+        static void addTaskMenu(QMenu *p_menu, Task *p_task);
+
+        static QToolBar *setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar);
+
         static QToolBar *setupSettingsToolBar(MainWindow *p_win, QToolBar *p_toolBar);
 
         static void updateQuickAccessMenu(QMenu *p_menu);

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است