import QtQml 2.0 import QOwnNotesTypes 1.0 /** * This script is used for manual interaction with Taskwarrior by either * 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 */ function init() { // export selected data to Taskwarrior script.registerCustomAction("exportToTaskwarrior", "Export selected list as Taskwarrior tasks", "Export to Taskwarrior"); // import selected projects from Taskwarrior script.registerCustomAction("importFromTaskwarrior", "Import tasks from Taskwarrior as a list", "Import from Taskwarrior"); } /** * Get selected text and separate it by lines. * * @returns array of strings representing separate lines */ function getSelectedTextAndSeparateByNewline() { var text = script.noteTextEditSelectedText(); var separatedByLines; // Consider Windows different newline method. if (script.platformIsWindows()) { separatedByLines = text.split('\r\n'); } else { separatedByLines = text.split('\n'); } return separatedByLines; } /** * Parse input string to get the project name and run supplied function if found. * * @param str input string which may contain project name * @param func function to be executed if the project name is found * * @returns boolean for if the project name was detected or not */ 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 = /^(#+)[\s*]?(.+)?[\s*]?$/i; var isProjectName = projectRegExp.exec(str); if (isProjectName) { var projectName = isProjectName[2]; // Header level is simply the number of "#" characters var headerLevel = isProjectName[1].length; func(projectName, headerLevel); return true; } } function logIfVerbose(str) { // Logs only, if `verbose` setting is enabled. if (verbose) { script.log(str); } } /** * This function is invoked when a custom action is triggered * in the menu or via button * * @param identifier string the identifier defined in registerCustomAction */ function customActionInvoked(identifier) { 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. // We are keeping the project name as a array of strings. We will concatenate them to // get the final projectName with nesting. var projectName = []; // The reference header level is useful in case selection does not start with single "#". var referenceHeaderLevel = 0; // For each line, we are gathering data to properly create tasks. getSelectedTextAndSeparateByNewline().forEach(function (line){ if (getProjectNameAndRun(line, function (proName, headerLevel) { logIfVerbose("Detected project name: " + proName); logIfVerbose("Detected header level: " + headerLevel); // No project detected yet - define initial reference header level. if (projectName.length === 0) { referenceHeaderLevel = headerLevel - 1; } // Project name on the same or lower hierarchy level means, that we need // to pop last or multiple project name segments. if (projectName.length + referenceHeaderLevel >= headerLevel) { logIfVerbose("Detected similar or lower header level"); 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; // We are trying to get the task description. // It should be started with either - (minus) or * (asterisk) to indicate list item. var taskRegExp = /^[\*\-][\s*]?(.+)[\s*]?$/; var taskDescription; var isTask = taskRegExp.exec(line); if (isTask) { var tags = []; taskDescription = isTask[1]; logIfVerbose("Detected task: " + taskDescription); var fetchTag; var tagExp = /^(.+)?[\s*]?\+(.+)$/; var currentTaskDescription = taskDescription; do { logIfVerbose("Fetching tags..."); fetchTag = tagExp.exec(currentTaskDescription); if (fetchTag) { logIfVerbose("Tag " + fetchTag[2] + " found!"); tags.push(fetchTag[2]); currentTaskDescription = fetchTag[1]; var re = new RegExp("\\+" + fetchTag[2].replace(/ /g, ''), "i"); taskDescription = taskDescription.replace(re,''); } else break; } while(currentTaskDescription); var concatenatedProjectName = projectName.join('.'); if (tags.length == 0) { logIfVerbose("Executing \"" + taskPath + " add pro:" + concatenatedProjectName + " " + taskDescription + "\""); script.startDetachedProcess(taskPath, [ "add", "pro:" + concatenatedProjectName, taskDescription ]); } else { logIfVerbose("Executing \"" + taskPath + " add pro:" + concatenatedProjectName + " " + taskDescription + " tags:\"" + tags.join(' ') + "\"\""); script.startDetachedProcess(taskPath, [ "add", "pro:" + concatenatedProjectName, taskDescription, "tags:\"" + tags.join(' ') + "\"" ]); } // We expect, that the task description would be the only thing in the line, hence `return`. return; } }); break; case "importFromTaskwarrior": // Get selected text to determine the project we want to import from. logIfVerbose("Importing tasks from Taskwarrior."); // Saving selected text - we will modify it later. var plainText = getSelectedTextAndSeparateByNewline(); var projectNames = []; var projectNameLines = []; var currentLineNo = 0; var referenceHeaderLevel = 0; plainText.forEach(function (line){ currentLineNo++; 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("Detected similar or lower header level"); 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; // We remember lines, that have project names in them. Those will be our anchors after // which we will insert fetched tasks. projectNameLines.push(currentLineNo); }); var currentProjectNumber = 0; projectNames.forEach( function(projectName) { currentProjectNumber++; var concatenatedProjectName = projectName.join('.'); var result = script.startSynchronousProcess(taskPath, [ "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(''); } 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) { logIfVerbose("No entries"); return; } tasksSeparated.splice(0, 1); // removing "Desc" tasksSeparated.splice(0, 1); // removing "----" tasksSeparated.splice(tasksSeparated.length - 1, 1); // removing "" tasksSeparated.splice(tasksSeparated.length - 1, 1); // removing "X tasks" tasksSeparated.splice(tasksSeparated.length - 1, 1); // removing "" var taskIds = []; tasksSeparated.forEach( function(task){ // Splitting output to be used later var taskParamsRegexp = /(\d+)[\s*]?(.+)?[\s*]?/i; var fetchTaskParams = taskParamsRegexp.exec(task); logIfVerbose("Extracted data from task: ID " + fetchTaskParams[1] + " Desc: " + fetchTaskParams[2]); // We are fetching tags to append them to description line var tagResult = script.startSynchronousProcess(taskPath, [ fetchTaskParams[1], "tag" ], ""); var tagsPlainText = ""; if (tagResult) { var tagsSeparated; // The result does not contain any \n, so we are splitting by whitespace. tagsSeparated = tagResult.toString().split('\n'); tagsSeparated.splice(0, 1); // removing "" if (tagsSeparated.length === 0) { logIfVerbose("No tags"); } else { tagsSeparated.splice(0, 1); // removing headline tagsSeparated.splice(0, 1); // removing "----" tagsSeparated.splice(tagsSeparated.length - 1, 1); // removing "" tagsSeparated.splice(tagsSeparated.length - 1, 1); // removing "" tagsSeparated.splice(tagsSeparated.length - 1, 1); // removing "" tagsSeparated.forEach( function(tag){ var tagsRegexp = /[\s*]?(.+)[\s*]?1[\s*]?/i; var fetchTag = tagsRegexp.exec(tag); tagsPlainText += " +" + fetchTag[1].replace(/ /g,''); }); } } var taskEntry = "* " + fetchTaskParams[2] + tagsPlainText; logIfVerbose("Inserting \"" + taskEntry + "\" after line " + projectNameLines[currentProjectNumber - 1]); plainText.splice(projectNameLines[currentProjectNumber - 1], 0, taskEntry); // We are updating line number assigned to detected project names. var i; for (i = currentProjectNumber; i < projectNameLines.length; i++) { projectNameLines[i] += 1; } // We gather task IDs in case deleteOnImport is enabled. taskIds.push(fetchTaskParams[1]); }); if (deleteOnImport) { logIfVerbose("Deleting tasks " + taskIds.join(',')); script.startDetachedProcess(taskPath, [ taskIds.join(' '), "delete" ]); } } }); // Finally, selected text is replaced by the text with insertions. script.noteTextEditWrite(plainText.join('\n')); break; } } }