Merge pull request #6 from fmakowski/master

Taskwarrior: user guide, better text insertion during import, small fixes
This commit is contained in:
Patrizio Bekerle 2017-06-11 22:27:27 +02:00 committed by GitHub
commit cb0da43ff5
3 changed files with 148 additions and 14 deletions

102
taskwarrior/README.md Normal file
View File

@ -0,0 +1,102 @@
QOwnNotes Taskwarrior
========================
**QOwnNotes Taskwarrior** utility script is a tool, that tries to simplify moving tasks between your notes and task entries in [Taskwarrior](http://taskwarrior.org).
# Installation
The recommended way of installing the script is using [QOwnNotes](http://qownnotes.org) script repository (available from `Script settings` under `Scripts` menu). The script is called *Taskwarrior*.
In case you want to modify the script, you may clone `qownnotes/scripts` repository and use `taskwarrior.qml` file directly, loading it as a local script in `Script settings`.
# Usage
## Export (note -> TW)
To export a set of tasks, you need to select text, and then click `Export to Taskwarrior` button (or go to `Scripts` menu for the menu entry). The following rules apply to selected text:
* project names should be formatted as headers, and may be nested
* the tasks are taken only from the unordered list items - lines that are starting with asterisk (*) or minus sign (-)
* any other content will be skipped - any additional comments or empty lines do not matter during the parsing process.
### Example
The following Markdown text:
## Project1
* task one
### Subproject1
* task two
Some additional portion of text, describing how awesome project 1 is
* task three
### Subproject2
* task four
# Project2
* task five
should generate following calls to Taskwarrior:
task add pro:Project1 task one
task add pro:Project1.Subproject1 task two
task add pro:Project1.Subroject1 task three
task add pro:Project1.Subproject2 task four
task add pro:Project2 task five
## Import (TW -> note)
Importing tasks requires listing of projects that tasks will be imported from. When writing a list of projects, please remember about the following rules:
* project names should be formatted as headers, and may be nested
* any other content will be skipped - any additional comments, tasks already written or empty lines do not matter during the parsing process.
* only the tasks from default report will be fetched - usually it means, that only pending tasks are imported
* any metadata of the task are not imported - project name and description are the only thing that are kept.
### Example
Following the example from `Export` section, we want to fetch tasks and import them.
Selected Markdown text:
# Project1
## Subproject2
* task four
## Subproject1
Something written here.
# Project3
# Project2
* older task not in Taskwarrior
will be replaced by:
# Project1
* task one
## Subproject2
* task four
* task four
## Subproject1
* task two
Something written here.
# Project3
# Project2
* task five
* older task not in Taskwarrior
As you can see, tasks are inserted just after the corresponding header. Other lines are not changed, which may result in duplicates (in case the task was already imported some other time, like in `Subproject2`).
# Settings
Script settings are available in `Script settings` under `Scripts` menu.
## Taskwarrior path
In case your Taskwarrior instance is not in default directory, you may change it to reflect the actual executable path.
## Delete on import
Although not enabled by default, a script may delete task entries from Taskwarrior when they are imported.
Please, be aware of the risk, that this feature might be bugged in some cases. If you encounter any issues, the recommended way of reverting the changes is using `task undo`. At some point, the Undo feature will be implemented to the script itself as well.
## Verbosity
If you want to monitor logger output in the Event log, it would be advisable to enable this option. Useful for debugging purposes.

View File

@ -2,9 +2,9 @@
"name": "Taskwarrior",
"identifier": "taskwarrior",
"script": "taskwarrior.qml",
"version": "0.0.3",
"version": "0.0.4",
"minAppVersion": "17.06.4",
"authors": ["@fmakowski"],
"platforms": ["linux", "macos"],
"description" : "This script creates menu items and buttons to import and export Taskwarrior tasks.\n\n<b>Dependencies</b>\n\nUnix-like OSes only!\n<a href=\"http://taskwarrior.org\">Taskwarrior</a>\n\n<b>Usage</b>\n\n<i>Export:</i>\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\n<i>Import:</i>\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.<b>To Do</b>\n\n * foolproof regexps\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\n<b>Dependencies</b>\n\nUnix-like OSes only!\n<a href=\"http://taskwarrior.org\">Taskwarrior</a>\n\n<b>Usage</b>\n\nFor usage description, please head to <a href=\"https://github.com/qownnotes/scripts/tree/master/taskwarrior\">script guide</a>."
}

View File

@ -41,7 +41,7 @@ QtObject {
// export selected data to Taskwarrior
script.registerCustomAction("exportToTaskwarrior", "Export selected list as Taskwarrior tasks", "Export to Taskwarrior");
// create a menu entry "Create meeting note" with a button and a freedesktop theme icon
// import selected projects from Taskwarrior
script.registerCustomAction("importFromTaskwarrior", "Import tasks from Taskwarrior as a list", "Import from Taskwarrior");
}
@ -73,10 +73,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 = /(#+)[\s*]?(.+)?[\s*]?/i;
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;
@ -84,6 +85,7 @@ QtObject {
}
function logIfVerbose(str) {
// Logs only, if `verbose` setting is enabled.
if (verbose) {
script.log(str);
}
@ -107,6 +109,7 @@ QtObject {
// 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.
@ -115,11 +118,15 @@ QtObject {
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();
@ -137,7 +144,7 @@ QtObject {
// 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 taskRegExp = /^[\*\-][\s*]?(.+)[\s*]?$/;
var taskDescription;
var isTask = taskRegExp.exec(line);
@ -162,11 +169,19 @@ QtObject {
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;
getSelectedTextAndSeparateByNewline().forEach(function (line){
if (getProjectNameAndRun(line, function (proName, headerLevel) {
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]);
@ -178,7 +193,7 @@ QtObject {
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.");
logIfVerbose("Detected similar or lower header level");
var i;
for (i = newProjectName.length + referenceHeaderLevel - headerLevel + 1; i > 0; i--) {
newProjectName.pop();
@ -193,12 +208,16 @@ QtObject {
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);
});
// To avoid overwriting what we have selected, we are simply writing it
script.noteTextEditWrite(script.noteTextEditSelectedText());
var currentProjectNumber = 0;
projectNames.forEach( function(projectName) {
currentProjectNumber++;
var concatenatedProjectName = projectName.join('.');
var result = script.startSynchronousProcess(taskPath,
[
@ -215,9 +234,7 @@ QtObject {
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');
@ -236,14 +253,25 @@ QtObject {
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]);
script.noteTextEditWrite("* " + fetchTaskParams[2] + "\n");
var taskEntry = "* " + fetchTaskParams[2];
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(' '),
@ -254,6 +282,10 @@ QtObject {
}
});
// Finally, selected text is replaced by the text with insertions.
script.noteTextEditWrite(plainText.join('\n'));
break;
}