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" + ]); + } }