mirror of
https://github.com/marty-oehme/scripts.git
synced 2024-11-14 13:58:07 +00:00
353 lines
17 KiB
QML
353 lines
17 KiB
QML
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;
|
|
|
|
}
|
|
}
|
|
}
|