diff --git a/taskwarrior/info.json b/taskwarrior/info.json
index 774291c..2df79ce 100644
--- a/taskwarrior/info.json
+++ b/taskwarrior/info.json
@@ -1,10 +1,10 @@
{
- "name": "Taskwarrior import/export",
+ "name": "Taskwarrior",
"identifier": "taskwarrior",
"script": "taskwarrior.qml",
- "version": "0.0.2",
- "minAppVersion": "17.06.1",
+ "version": "0.0.3",
+ "minAppVersion": "17.06.4",
"authors": ["@fmakowski"],
"platforms": ["linux", "macos"],
- "description" : "This script creates menu items and buttons to import and export Taskwarrior tasks.\n\nDependencies\n\nUnix-like OSes only!\nTaskwarrior\n\nUsage\n\nExport:\nThe script takes selected text from your note and parse it to create task entries based on it. The following rules currently apply for the test to be parsed correctly:\n * the project is defined by writing \"project:\" (case-insensitive) immediately followed by the project name; the rest of the line content is skipped\n * the task is defined by making a list item, using either - (minus) or * (asterisk) at the beginning; the task description taken will be used with the most recently detected project name to create a new task\n\nImport:\nThe script takes selected text from your note, parsing it into the project names you want to fetch from Taskwarrior into the note. The tasks will be written as a list right below the selection. The project names will be appended before the lists As \"Project: [projectName]\" to separate lists.To Do\n\n * foolproof used regexps\n * make project nesting easier\n * use headers to determine project name and nesting\n * Windows support\n * Taskwarrior parameter parsing (like tags, dates, priority, etc.)"
+ "description" : "This script creates menu items and buttons to import and export Taskwarrior tasks.\n\nDependencies\n\nUnix-like OSes only!\nTaskwarrior\n\nUsage\n\nExport:\nThe script takes selected text from your note and parse it to create task entries based on it. The following rules currently apply for the test to be parsed correctly:\n * the project is defined as the header. Subheaders are appended to hierarchically upper header name (giving the possibility of nesting) * the task is defined by making a list item, using either - (minus) or * (asterisk) at the beginning; the task description taken will be used with the most recently detected project name to create a new task\n\nImport:\nThe script takes selected text from your note, parsing it into the project names you want to fetch from Taskwarrior into the note. The tasks will be written as a list right below the selection. The project names will be created as headers to separate tasks, with appropriate nesting.To Do\n\n * foolproof regexps\n * Windows support\n * Taskwarrior parameter parsing (like tags, dates, priority, etc.)"
}
diff --git a/taskwarrior/taskwarrior.qml b/taskwarrior/taskwarrior.qml
index 57e1908..0544795 100644
--- a/taskwarrior/taskwarrior.qml
+++ b/taskwarrior/taskwarrior.qml
@@ -6,6 +6,34 @@ import QOwnNotesTypes 1.0
* importing tasks from a certain project, or exporting them from a note.
*/
QtObject {
+ property string taskPath;
+ property bool verbose;
+ property bool deleteOnImport;
+
+ property variant settingsVariables: [
+ {
+ "identifier": "taskPath",
+ "name": "Taskwarrior path",
+ "description": "A path to your Taskwarrior instance",
+ "type": "string",
+ "default": "/usr/bin/task",
+ },
+ {
+ "identifier": "verbose",
+ "name": "Verbose logging",
+ "description": "Should the script log every action?",
+ "type": "boolean",
+ "default": false
+ },
+ {
+ "identifier": "deleteOnImport",
+ "name": "Delete on import",
+ "description": "Delete tasks on import?",
+ "type": "boolean",
+ "default": false
+ }
+ ];
+
/**
* Initializes the custom actions
*/
@@ -45,14 +73,22 @@ QtObject {
function getProjectNameAndRun(str, func) {
// We are trying to get the name of the project.
// To do so, we are getting the substring of a line by using regexp group.
- var projectRegExp = /project:[\s*]?(.+)?[\s*]?/i;
+ var projectRegExp = /(#+)[\s*]?(.+)?[\s*]?/i;
var isProjectName = projectRegExp.exec(str);
if (isProjectName) {
- func(isProjectName[1]);
+ var projectName = isProjectName[2];
+ var headerLevel = isProjectName[1].length;
+ func(projectName, headerLevel);
return true;
}
}
+ function logIfVerbose(str) {
+ if (verbose) {
+ script.log(str);
+ }
+ }
+
/**
* This function is invoked when a custom action is triggered
* in the menu or via button
@@ -60,21 +96,41 @@ QtObject {
* @param identifier string the identifier defined in registerCustomAction
*/
function customActionInvoked(identifier) {
-
- var pathToTaskwarrior = "/usr/bin/task";
-
switch (identifier) {
// export selected lines to Taskwarriors as tasks.
// The project name will be taken from "Project:" keyword detected in first lines.
case "exportToTaskwarrior":
+ logIfVerbose("Exporting tasks from a note.");
+
// Starting with an empty default project name.
- var projectName = "";
-
+ // We are keeping the project name as a array of strings. We will concatenate them to
+ // get the final projectName with nesting.
+ var projectName = [];
+ var referenceHeaderLevel = 0;
+
// For each line, we are gathering data to properly create tasks.
getSelectedTextAndSeparateByNewline().forEach(function (line){
- if (getProjectNameAndRun(line, function (proName) {
- projectName = proName;
+ if (getProjectNameAndRun(line, function (proName, headerLevel) {
+ logIfVerbose("Detected project name: " + proName);
+ logIfVerbose("Detected header level: " + headerLevel);
+
+ if (projectName.length === 0) {
+ referenceHeaderLevel = headerLevel - 1;
+ }
+
+ if (projectName.length + referenceHeaderLevel >= headerLevel) {
+ var i;
+ for (i = projectName.length + referenceHeaderLevel - headerLevel + 1; i > 0; i--) {
+ projectName.pop();
+ if (projectName.length === 0) {
+ referenceHeaderLevel = headerLevel - 1;
+ break;
+ }
+ }
+ }
+ projectName.push(proName);
+
// We expect, that the project name would be the only thing in line, hence `return`.
return;
})) return;
@@ -86,11 +142,15 @@ QtObject {
var isTask = taskRegExp.exec(line);
if (isTask) {
+
taskDescription = isTask[1];
- script.startDetachedProcess(pathToTaskwarrior,
+ logIfVerbose("Detected task: " + taskDescription);
+ var concatenatedProjectName = projectName.join('.');
+ logIfVerbose("Executing \"" + taskPath + " add pro:" + concatenatedProjectName + " " + taskDescription + "\"");
+ script.startDetachedProcess(taskPath,
[
"add",
- "pro:" + projectName,
+ "pro:" + concatenatedProjectName,
taskDescription
]);
// We expect, that the task description would be the only thing in the line, hence `return`.
@@ -103,10 +163,35 @@ QtObject {
// Get selected text to determine the project we want to import from.
var projectNames = [];
+ var referenceHeaderLevel = 0;
getSelectedTextAndSeparateByNewline().forEach(function (line){
- if (getProjectNameAndRun(line, function (proName) {
- projectNames.push(proName);
+ if (getProjectNameAndRun(line, function (proName, headerLevel) {
+ if (projectNames.length === 0) {
+ logIfVerbose("No project detected yet. Inserting " + proName)
+ projectNames.push([proName]);
+ logIfVerbose("Reference header level set to " + headerLevel)
+ referenceHeaderLevel = headerLevel - 1;
+ return;
+ }
+
+ var newProjectName = projectNames[projectNames.length - 1].slice();
+ logIfVerbose("Last project name inserted was " + newProjectName.join('.'));
+ if (newProjectName.length + referenceHeaderLevel >= headerLevel) {
+ logIfVerbose("Same header level detected.");
+ var i;
+ for (i = newProjectName.length + referenceHeaderLevel - headerLevel + 1; i > 0; i--) {
+ newProjectName.pop();
+ if (newProjectName.length === 0) {
+ referenceHeaderLevel = headerLevel - 1;
+ break;
+ }
+ }
+ }
+ newProjectName.push(proName);
+ projectNames.push(newProjectName);
+ logIfVerbose("Project name detected. Inserted value is " + newProjectName.join('.'))
+
})) return;
});
@@ -114,21 +199,32 @@ QtObject {
script.noteTextEditWrite(script.noteTextEditSelectedText());
projectNames.forEach( function(projectName) {
- var result = script.startSynchronousProcess(pathToTaskwarrior,
+ var concatenatedProjectName = projectName.join('.');
+ var result = script.startSynchronousProcess(taskPath,
[
- "pro:" + projectName,
- "rc.report.next.columns=description",
- "rc.report.next.labels=Desc"
+ "pro.is:" + concatenatedProjectName,
+ "rc.report.next.columns=id,description.desc",
+ "rc.report.next.labels=ID,Desc"
],
"");
if (result) {
+ // via https://stackoverflow.com/a/35635260
+ var repeat = function(str, count) {
+ var array = [];
+ for(var i = 0; i < count;)
+ array[i++] = str;
+ return array.join('');
+ }
+
+ script.noteTextEditWrite("\n");
+ script.noteTextEditWrite(repeat('#', projectName.length) + ' ' + projectName[projectName.length - 1] + "\n\n");
var tasksSeparated;
// The result does not contain any \n, so we are splitting by whitespace.
tasksSeparated = result.toString().split('\n');
tasksSeparated.splice(0, 1); // removing ""
if (tasksSeparated.length === 0) {
- script.log("No entries");
+ logIfVerbose("No entries");
return;
}
tasksSeparated.splice(0, 1); // removing "Desc"
@@ -138,12 +234,22 @@ QtObject {
tasksSeparated.splice(tasksSeparated.length - 1, 1); // removing "X tasks"
tasksSeparated.splice(tasksSeparated.length - 1, 1); // removing ""
- script.noteTextEditWrite("\n");
-
- script.noteTextEditWrite("Project: " + projectName + "\n\n");
- tasksSeparated.forEach( function(taskDesc){
- script.noteTextEditWrite("* " + taskDesc + "\n");
+ var taskIds = [];
+ tasksSeparated.forEach( function(task){
+ var taskParamsRegexp = /(\d+)[\s*]?(.+)?[\s*]?/i;
+ var fetchTaskParams = taskParamsRegexp.exec(task);
+ logIfVerbose("Extracted data from task: ID " + fetchTaskParams[1] + " Desc: " + fetchTaskParams[2]);
+ script.noteTextEditWrite("* " + fetchTaskParams[2] + "\n");
+ taskIds.push(fetchTaskParams[1]);
});
+
+ if (deleteOnImport) {
+ script.startDetachedProcess(taskPath,
+ [
+ taskIds.join(' '),
+ "delete"
+ ]);
+ }
}