From 4a2e61d61b6d69f89b9e785a7fa56d543410daf7 Mon Sep 17 00:00:00 2001 From: Filip Makowski Date: Mon, 5 Jun 2017 12:19:09 +0200 Subject: [PATCH 1/7] Added header parsing (no need to use "project:" statement anymore for project definition) --- taskwarrior/info.json | 2 +- taskwarrior/taskwarrior.qml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/taskwarrior/info.json b/taskwarrior/info.json index 774291c..e19d864 100644 --- a/taskwarrior/info.json +++ b/taskwarrior/info.json @@ -6,5 +6,5 @@ "minAppVersion": "17.06.1", "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 by writing \"project:\" (case-insensitive) immediately followed by the project name; the rest of the line content is skipped\n * the headers (lines starting with #) are also considered as project names - no nesting available yet\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 * Windows support\n * Taskwarrior parameter parsing (like tags, dates, priority, etc.)" } diff --git a/taskwarrior/taskwarrior.qml b/taskwarrior/taskwarrior.qml index 57e1908..5a1c904 100644 --- a/taskwarrior/taskwarrior.qml +++ b/taskwarrior/taskwarrior.qml @@ -45,10 +45,11 @@ 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 = /(project:[\s*]?(.+)?[\s*]?)|(#+[\s*]?(.+)?[\s*]?)/i; var isProjectName = projectRegExp.exec(str); if (isProjectName) { - func(isProjectName[1]); + var projectName = isProjectName[2] ? isProjectName[2] : isProjectName[4]; + func(projectName); return true; } } From 5a951fdbf47fb2efc838244d89d1926ab0edadfc Mon Sep 17 00:00:00 2001 From: Filip Makowski Date: Sun, 11 Jun 2017 15:43:20 +0200 Subject: [PATCH 2/7] Parametrized Taskwarrior executable path (to be set in settings). --- taskwarrior/taskwarrior.qml | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/taskwarrior/taskwarrior.qml b/taskwarrior/taskwarrior.qml index 5a1c904..c9a380e 100644 --- a/taskwarrior/taskwarrior.qml +++ b/taskwarrior/taskwarrior.qml @@ -6,6 +6,18 @@ import QOwnNotesTypes 1.0 * importing tasks from a certain project, or exporting them from a note. */ QtObject { + property string myString; + + property variant settingsVariables: [ + { + "identifier": "taskPath", + "name": "Taskwarrior path", + "description": "A path to your Taskwarrior instance", + "type": "string", + "default": "/usr/bin/task", + } + ]; + /** * Initializes the custom actions */ @@ -61,9 +73,6 @@ 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. @@ -88,7 +97,7 @@ QtObject { var isTask = taskRegExp.exec(line); if (isTask) { taskDescription = isTask[1]; - script.startDetachedProcess(pathToTaskwarrior, + script.startDetachedProcess(taskPath, [ "add", "pro:" + projectName, @@ -115,10 +124,10 @@ QtObject { script.noteTextEditWrite(script.noteTextEditSelectedText()); projectNames.forEach( function(projectName) { - var result = script.startSynchronousProcess(pathToTaskwarrior, + var result = script.startSynchronousProcess(taskPath, [ - "pro:" + projectName, - "rc.report.next.columns=description", + "pro.is:" + projectName, + "rc.report.next.columns=description.desc", "rc.report.next.labels=Desc" ], ""); @@ -141,7 +150,7 @@ QtObject { script.noteTextEditWrite("\n"); - script.noteTextEditWrite("Project: " + projectName + "\n\n"); + script.noteTextEditWrite("# " + projectName + "\n\n"); tasksSeparated.forEach( function(taskDesc){ script.noteTextEditWrite("* " + taskDesc + "\n"); }); From abf76709275ee04f0e54b333e383594ce302ba36 Mon Sep 17 00:00:00 2001 From: Filip Makowski Date: Sun, 11 Jun 2017 16:09:49 +0200 Subject: [PATCH 3/7] Added verbose setting. Also added several log calls in the code. --- taskwarrior/taskwarrior.qml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/taskwarrior/taskwarrior.qml b/taskwarrior/taskwarrior.qml index c9a380e..1e62179 100644 --- a/taskwarrior/taskwarrior.qml +++ b/taskwarrior/taskwarrior.qml @@ -6,7 +6,8 @@ import QOwnNotesTypes 1.0 * importing tasks from a certain project, or exporting them from a note. */ QtObject { - property string myString; + property string taskPath; + property bool verbose; property variant settingsVariables: [ { @@ -15,6 +16,13 @@ QtObject { "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 } ]; @@ -66,6 +74,12 @@ QtObject { } } + function logIfVerbose(str) { + if (verbose) { + script.log(str); + } + } + /** * This function is invoked when a custom action is triggered * in the menu or via button @@ -78,12 +92,15 @@ QtObject { // 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 = ""; // For each line, we are gathering data to properly create tasks. getSelectedTextAndSeparateByNewline().forEach(function (line){ if (getProjectNameAndRun(line, function (proName) { + logIfVerbose("Detected project name: " + proName); projectName = proName; // We expect, that the project name would be the only thing in line, hence `return`. return; @@ -96,7 +113,10 @@ QtObject { var isTask = taskRegExp.exec(line); if (isTask) { + taskDescription = isTask[1]; + logIfVerbose("Detected task: " + taskDescription); + logIfVerbose("Executing \"" + taskPath + " add pro:" + projectName + " " + taskDescription + "\""); script.startDetachedProcess(taskPath, [ "add", @@ -138,7 +158,7 @@ QtObject { tasksSeparated.splice(0, 1); // removing "" if (tasksSeparated.length === 0) { - script.log("No entries"); + logIfVerbose("No entries"); return; } tasksSeparated.splice(0, 1); // removing "Desc" From 7bd91852f93113f08c7a7cdbd5d5c9c99c95b3f8 Mon Sep 17 00:00:00 2001 From: Filip Makowski Date: Sun, 11 Jun 2017 18:07:06 +0200 Subject: [PATCH 4/7] Added header nesting for project name parsing. --- taskwarrior/taskwarrior.qml | 38 +++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/taskwarrior/taskwarrior.qml b/taskwarrior/taskwarrior.qml index 1e62179..e834a73 100644 --- a/taskwarrior/taskwarrior.qml +++ b/taskwarrior/taskwarrior.qml @@ -65,11 +65,12 @@ 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*]?)|(#+[\s*]?(.+)?[\s*]?)/i; + var projectRegExp = /(#+)[\s*]?(.+)?[\s*]?/i; var isProjectName = projectRegExp.exec(str); if (isProjectName) { - var projectName = isProjectName[2] ? isProjectName[2] : isProjectName[4]; - func(projectName); + var projectName = isProjectName[2]; + var headerLevel = isProjectName[1].length; + func(projectName, headerLevel); return true; } } @@ -95,13 +96,29 @@ QtObject { 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) { + if (getProjectNameAndRun(line, function (proName, headerLevel) { logIfVerbose("Detected project name: " + proName); - projectName = 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(); + } + } + projectName.push(proName); + // We expect, that the project name would be the only thing in line, hence `return`. return; })) return; @@ -116,11 +133,12 @@ QtObject { taskDescription = isTask[1]; logIfVerbose("Detected task: " + taskDescription); - logIfVerbose("Executing \"" + taskPath + " add pro:" + projectName + " " + 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`. @@ -135,7 +153,7 @@ QtObject { var projectNames = []; getSelectedTextAndSeparateByNewline().forEach(function (line){ - if (getProjectNameAndRun(line, function (proName) { + if (getProjectNameAndRun(line, function (proName, headerLevel) { projectNames.push(proName); })) return; }); From ae141f1c6e09f7514e0f3797d0b4524f426f108f Mon Sep 17 00:00:00 2001 From: Filip Makowski Date: Sun, 11 Jun 2017 18:45:15 +0200 Subject: [PATCH 5/7] Nested headers parsing available during task importing. --- taskwarrior/taskwarrior.qml | 47 +++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/taskwarrior/taskwarrior.qml b/taskwarrior/taskwarrior.qml index e834a73..447cf0a 100644 --- a/taskwarrior/taskwarrior.qml +++ b/taskwarrior/taskwarrior.qml @@ -115,6 +115,10 @@ QtObject { 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); @@ -151,10 +155,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, headerLevel) { - projectNames.push(proName); + 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; }); @@ -162,14 +191,25 @@ QtObject { script.noteTextEditWrite(script.noteTextEditSelectedText()); projectNames.forEach( function(projectName) { + var concatenatedProjectName = projectName.join('.'); var result = script.startSynchronousProcess(taskPath, [ - "pro.is:" + projectName, + "pro.is:" + concatenatedProjectName, "rc.report.next.columns=description.desc", "rc.report.next.labels=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'); @@ -186,9 +226,6 @@ QtObject { tasksSeparated.splice(tasksSeparated.length - 1, 1); // removing "X tasks" tasksSeparated.splice(tasksSeparated.length - 1, 1); // removing "" - script.noteTextEditWrite("\n"); - - script.noteTextEditWrite("# " + projectName + "\n\n"); tasksSeparated.forEach( function(taskDesc){ script.noteTextEditWrite("* " + taskDesc + "\n"); }); From 3d5140c7e54b99dad8bd13f6fb1f0d620391ce69 Mon Sep 17 00:00:00 2001 From: Filip Makowski Date: Sun, 11 Jun 2017 18:53:41 +0200 Subject: [PATCH 6/7] Updated description and bumped up version. --- taskwarrior/info.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/taskwarrior/info.json b/taskwarrior/info.json index e19d864..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 headers (lines starting with #) are also considered as project names - no nesting available yet\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 * 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.)" } From 85055c39aa924acf77143a3fc44ee5034849d322 Mon Sep 17 00:00:00 2001 From: Filip Makowski Date: Sun, 11 Jun 2017 19:35:02 +0200 Subject: [PATCH 7/7] Added "Delete on import" setting, false by default. --- taskwarrior/taskwarrior.qml | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/taskwarrior/taskwarrior.qml b/taskwarrior/taskwarrior.qml index 447cf0a..0544795 100644 --- a/taskwarrior/taskwarrior.qml +++ b/taskwarrior/taskwarrior.qml @@ -8,6 +8,7 @@ import QOwnNotesTypes 1.0 QtObject { property string taskPath; property bool verbose; + property bool deleteOnImport; property variant settingsVariables: [ { @@ -23,6 +24,13 @@ QtObject { "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 } ]; @@ -195,8 +203,8 @@ QtObject { var result = script.startSynchronousProcess(taskPath, [ "pro.is:" + concatenatedProjectName, - "rc.report.next.columns=description.desc", - "rc.report.next.labels=Desc" + "rc.report.next.columns=id,description.desc", + "rc.report.next.labels=ID,Desc" ], ""); if (result) { @@ -226,9 +234,22 @@ QtObject { tasksSeparated.splice(tasksSeparated.length - 1, 1); // removing "X tasks" tasksSeparated.splice(tasksSeparated.length - 1, 1); // removing "" - 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" + ]); + } }