mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
1218 lines
34 KiB
C++
1218 lines
34 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
|
// task - a command line task list manager.
|
|
//
|
|
// Copyright 2006 - 2009, Paul Beckingham.
|
|
// All rights reserved.
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify it under
|
|
// the terms of the GNU General Public License as published by the Free Software
|
|
// Foundation; either version 2 of the License, or (at your option) any later
|
|
// version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
// details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along with
|
|
// this program; if not, write to the
|
|
//
|
|
// Free Software Foundation, Inc.,
|
|
// 51 Franklin Street, Fifth Floor,
|
|
// Boston, MA
|
|
// 02110-1301
|
|
// USA
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include "Date.h"
|
|
#include "text.h"
|
|
#include "util.h"
|
|
#include "main.h"
|
|
|
|
extern Context context;
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
enum fileType
|
|
{
|
|
not_a_clue,
|
|
task_1_4_3,
|
|
task_1_5_0,
|
|
task_1_6_0,
|
|
task_cmd_line,
|
|
todo_sh_2_0,
|
|
csv,
|
|
text
|
|
};
|
|
|
|
static fileType determineFileType (const std::vector <std::string>& lines)
|
|
{
|
|
// '7f7a4191-c2f2-487f-8855-7a1eb378c267',' ...
|
|
// ....:....|....:....|....:....|....:....|
|
|
// 1 10 20 30 40
|
|
if (lines.size () > 1 &&
|
|
lines[1][0] == '\'' &&
|
|
lines[1][9] == '-' &&
|
|
lines[1][14] == '-' &&
|
|
lines[1][19] == '-' &&
|
|
lines[1][24] == '-' &&
|
|
lines[1][37] == '\'' &&
|
|
lines[1][38] == ',' &&
|
|
lines[1][39] == '\'')
|
|
{
|
|
if (lines[0] == "'uuid','status','tags','entry','start','due','recur',"
|
|
"'end','project','priority','fg','bg','description'")
|
|
return task_1_6_0;
|
|
|
|
if (lines[0] == "'id','uuid','status','tags','entry','start','due','recur',"
|
|
"'end','project','priority','fg','bg','description'")
|
|
return task_1_5_0;
|
|
|
|
if (lines[0] == "'id','status','tags','entry','start','due','end','project',"
|
|
"'priority','fg','bg','description'")
|
|
return task_1_4_3;
|
|
}
|
|
|
|
// A task command line might include a priority or project.
|
|
for (unsigned int i = 0; i < lines.size (); ++i)
|
|
{
|
|
std::vector <std::string> words;
|
|
split (words, lines[i], ' ');
|
|
|
|
for (unsigned int w = 0; w < words.size (); ++w)
|
|
if (words[w].substr (0, 9) == "priority:" ||
|
|
words[w].substr (0, 8) == "priorit:" ||
|
|
words[w].substr (0, 7) == "priori:" ||
|
|
words[w].substr (0, 6) == "prior:" ||
|
|
words[w].substr (0, 5) == "prio:" ||
|
|
words[w].substr (0, 4) == "pri:" ||
|
|
words[w].substr (0, 8) == "project:" ||
|
|
words[w].substr (0, 7) == "projec:" ||
|
|
words[w].substr (0, 6) == "proje:" ||
|
|
words[w].substr (0, 5) == "proj:" ||
|
|
words[w].substr (0, 4) == "pro:")
|
|
return task_cmd_line;
|
|
}
|
|
|
|
// x 2009-03-25 Walk the dog +project @context
|
|
// This is a test +project @context
|
|
for (unsigned int i = 0; i < lines.size (); ++i)
|
|
{
|
|
// All done tasks begin with "x YYYY-MM-DD".
|
|
if (lines[i].length () > 12)
|
|
{
|
|
if ( lines[i][0] == 'x' &&
|
|
lines[i][1] == ' ' &&
|
|
::isdigit (lines[i][2]) &&
|
|
::isdigit (lines[i][3]) &&
|
|
::isdigit (lines[i][4]) &&
|
|
::isdigit (lines[i][5]) &&
|
|
lines[i][6] == '-' &&
|
|
::isdigit (lines[i][7]) &&
|
|
::isdigit (lines[i][8]) &&
|
|
lines[i][9] == '-' &&
|
|
::isdigit (lines[i][10]) &&
|
|
::isdigit (lines[i][11]))
|
|
return todo_sh_2_0;
|
|
}
|
|
|
|
std::vector <std::string> words;
|
|
split (words, lines[i], ' ');
|
|
for (unsigned int w = 0; w < words.size (); ++w)
|
|
{
|
|
// +project
|
|
if (words[w].length () > 1 &&
|
|
words[w][0] == '+' &&
|
|
::isalnum (words[w][1]))
|
|
return todo_sh_2_0;
|
|
|
|
// @context
|
|
if (words[w].length () > 1 &&
|
|
words[w][0] == '@' &&
|
|
::isalnum (words[w][1]))
|
|
return todo_sh_2_0;
|
|
}
|
|
}
|
|
|
|
// CSV - commas on every non-comment, non-trivial line.
|
|
bool commas_on_every_line = true;
|
|
for (unsigned int i = 0; i < lines.size (); ++i)
|
|
{
|
|
if (lines[i].length () > 10 &&
|
|
lines[i].find (",") == std::string::npos)
|
|
{
|
|
commas_on_every_line = false;
|
|
break;
|
|
}
|
|
}
|
|
if (commas_on_every_line)
|
|
return csv;
|
|
|
|
// Looks like 'text' is the default case, if there is any data at all.
|
|
if (lines.size () > 1)
|
|
return text;
|
|
|
|
return not_a_clue;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static void decorateTask (Task& task)
|
|
{
|
|
char entryTime[16];
|
|
sprintf (entryTime, "%u", (unsigned int) time (NULL));
|
|
task.set ("entry", entryTime);
|
|
|
|
task.setStatus (Task::pending);
|
|
|
|
// Override with default.project, if not specified.
|
|
std::string defaultProject = context.config.get ("default.project", "");
|
|
if (!task.has ("project") && defaultProject != "")
|
|
task.set ("project", defaultProject);
|
|
|
|
// Override with default.priority, if not specified.
|
|
std::string defaultPriority = context.config.get ("default.priority", "");
|
|
if (!task.has ("priority") &&
|
|
defaultPriority != "" &&
|
|
Att::validNameValue ("priority", "", defaultPriority))
|
|
task.set ("priority", defaultPriority);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static std::string importTask_1_4_3 (const std::vector <std::string>& lines)
|
|
{
|
|
std::vector <std::string> failed;
|
|
|
|
context.tdb.lock (context.config.get ("locking", true));
|
|
|
|
std::vector <std::string>::const_iterator it;
|
|
for (it = lines.begin (); it != lines.end (); ++it)
|
|
{
|
|
try
|
|
{
|
|
// Skip the first line, if it is a columns header line.
|
|
if (it->substr (0, 5) == "'id',")
|
|
continue;
|
|
|
|
std::vector <std::string> fields;
|
|
split (fields, *it, ',');
|
|
|
|
// If there is an unexpected number of fields, something is wrong. Perhaps
|
|
// an embedded comma, in which case there are (at least) two fields that
|
|
// need to be concatenated.
|
|
if (fields.size () > 12)
|
|
{
|
|
int safety = 10; // Shouldn't be more than 10 commas in a description/project.
|
|
|
|
do
|
|
{
|
|
std::vector <std::string> modified;
|
|
for (unsigned int f = 0; f < fields.size (); ++f)
|
|
{
|
|
if (fields[f][0] != '\'' &&
|
|
fields[f][fields[f].length () - 1] == '\'')
|
|
{
|
|
modified[modified.size () - 1] += "," + fields[f];
|
|
}
|
|
|
|
else
|
|
modified.push_back (fields[f]);
|
|
}
|
|
fields = modified;
|
|
|
|
if (safety-- <= 0)
|
|
throw "unrecoverable";
|
|
}
|
|
while (fields.size () > 12);
|
|
}
|
|
|
|
if (fields.size () < 12)
|
|
throw "unrecoverable";
|
|
|
|
// Build up this task ready for insertion.
|
|
Task task;
|
|
|
|
// Handle the 12 fields.
|
|
for (unsigned int f = 0; f < fields.size (); ++f)
|
|
{
|
|
switch (f)
|
|
{
|
|
case 0: // 'uuid'
|
|
task.set ("uuid", fields[f].substr (1, 36));
|
|
break;
|
|
|
|
case 1: // 'status'
|
|
if (fields[f] == "'pending'") task.setStatus (Task::pending);
|
|
else if (fields[f] == "'recurring'") task.setStatus (Task::recurring);
|
|
else if (fields[f] == "'deleted'") task.setStatus (Task::deleted);
|
|
else if (fields[f] == "'completed'") task.setStatus (Task::completed);
|
|
break;
|
|
|
|
case 2: // 'tags'
|
|
if (fields[f].length () > 2)
|
|
{
|
|
std::string tokens = fields[f].substr (1, fields[f].length () - 2);
|
|
std::vector <std::string> tags;
|
|
split (tags, tokens, ' ');
|
|
for (unsigned int i = 0; i < tags.size (); ++i)
|
|
task.addTag (tags[i]);
|
|
}
|
|
break;
|
|
|
|
case 3: // entry
|
|
task.set ("entry", fields[f]);
|
|
break;
|
|
|
|
case 4: // start
|
|
if (fields[f].length ())
|
|
task.set ("start", fields[f]);
|
|
break;
|
|
|
|
case 5: // due
|
|
if (fields[f].length ())
|
|
task.set ("due", fields[f]);
|
|
break;
|
|
|
|
case 6: // end
|
|
if (fields[f].length ())
|
|
task.set ("end", fields[f]);
|
|
break;
|
|
|
|
case 7: // 'project'
|
|
if (fields[f].length () > 2)
|
|
task.set ("project", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 8: // 'priority'
|
|
if (fields[f].length () > 2)
|
|
task.set ("priority", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 9: // 'fg'
|
|
if (fields[f].length () > 2)
|
|
task.set ("fg", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 10: // 'bg'
|
|
if (fields[f].length () > 2)
|
|
task.set ("bg", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 11: // 'description'
|
|
if (fields[f].length () > 2)
|
|
task.set ("description", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
}
|
|
}
|
|
|
|
context.tdb.add (task);
|
|
}
|
|
|
|
catch (...)
|
|
{
|
|
failed.push_back (*it);
|
|
}
|
|
}
|
|
|
|
context.tdb.commit ();
|
|
context.tdb.unlock ();
|
|
|
|
std::stringstream out;
|
|
out << "Imported "
|
|
<< (lines.size () - failed.size () - 1)
|
|
<< " tasks successfully, with "
|
|
<< failed.size ()
|
|
<< " errors."
|
|
<< std::endl;
|
|
|
|
if (failed.size ())
|
|
{
|
|
std::string bad;
|
|
join (bad, "\n", failed);
|
|
return out.str () + "\nCould not import:\n\n" + bad;
|
|
}
|
|
|
|
return out.str ();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static std::string importTask_1_5_0 (const std::vector <std::string>& lines)
|
|
{
|
|
std::vector <std::string> failed;
|
|
|
|
context.tdb.lock (context.config.get ("locking", true));
|
|
|
|
std::vector <std::string>::const_iterator it;
|
|
for (it = lines.begin (); it != lines.end (); ++it)
|
|
{
|
|
try
|
|
{
|
|
// Skip the first line, if it is a columns header line.
|
|
if (it->substr (0, 5) == "'id',")
|
|
continue;
|
|
|
|
std::vector <std::string> fields;
|
|
split (fields, *it, ',');
|
|
|
|
// If there is an unexpected number of fields, something is wrong. Perhaps
|
|
// an embedded comma, in which case there are (at least) two fields that
|
|
// need to be concatenated.
|
|
if (fields.size () > 13)
|
|
{
|
|
int safety = 10; // Shouldn't be more than 10 commas in a description/project.
|
|
|
|
do
|
|
{
|
|
std::vector <std::string> modified;
|
|
for (unsigned int f = 0; f < fields.size (); ++f)
|
|
{
|
|
if (fields[f][0] != '\'' &&
|
|
fields[f][fields[f].length () - 1] == '\'')
|
|
{
|
|
modified[modified.size () - 1] += "," + fields[f];
|
|
}
|
|
|
|
else
|
|
modified.push_back (fields[f]);
|
|
}
|
|
fields = modified;
|
|
|
|
if (safety-- <= 0)
|
|
throw "unrecoverable";
|
|
}
|
|
while (fields.size () > 13);
|
|
}
|
|
|
|
if (fields.size () < 13)
|
|
throw "unrecoverable";
|
|
|
|
// Build up this task ready for insertion.
|
|
Task task;
|
|
|
|
// Handle the 13 fields.
|
|
for (unsigned int f = 0; f < fields.size (); ++f)
|
|
{
|
|
switch (f)
|
|
{
|
|
case 0: // 'uuid'
|
|
task.set ("uuid", fields[f].substr (1, 36));
|
|
break;
|
|
|
|
case 1: // 'status'
|
|
if (fields[f] == "'pending'") task.setStatus (Task::pending);
|
|
else if (fields[f] == "'recurring'") task.setStatus (Task::recurring);
|
|
else if (fields[f] == "'deleted'") task.setStatus (Task::deleted);
|
|
else if (fields[f] == "'completed'") task.setStatus (Task::completed);
|
|
break;
|
|
|
|
case 2: // 'tags'
|
|
if (fields[f].length () > 2)
|
|
{
|
|
std::string tokens = fields[f].substr (1, fields[f].length () - 2);
|
|
std::vector <std::string> tags;
|
|
split (tags, tokens, ' ');
|
|
for (unsigned int i = 0; i < tags.size (); ++i)
|
|
task.addTag (tags[i]);
|
|
}
|
|
break;
|
|
|
|
case 3: // entry
|
|
task.set ("entry", fields[f]);
|
|
break;
|
|
|
|
case 4: // start
|
|
if (fields[f].length ())
|
|
task.set ("start", fields[f]);
|
|
break;
|
|
|
|
case 5: // due
|
|
if (fields[f].length ())
|
|
task.set ("due", fields[f]);
|
|
break;
|
|
|
|
case 6: // recur
|
|
if (fields[f].length ())
|
|
task.set ("recur", fields[f]);
|
|
break;
|
|
|
|
case 7: // end
|
|
if (fields[f].length ())
|
|
task.set ("end", fields[f]);
|
|
break;
|
|
|
|
case 8: // 'project'
|
|
if (fields[f].length () > 2)
|
|
task.set ("project", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 9: // 'priority'
|
|
if (fields[f].length () > 2)
|
|
task.set ("priority", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 10: // 'fg'
|
|
if (fields[f].length () > 2)
|
|
task.set ("fg", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 11: // 'bg'
|
|
if (fields[f].length () > 2)
|
|
task.set ("bg", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 12: // 'description'
|
|
if (fields[f].length () > 2)
|
|
task.set ("description", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
}
|
|
}
|
|
|
|
context.tdb.add (task);
|
|
}
|
|
|
|
catch (...)
|
|
{
|
|
failed.push_back (*it);
|
|
}
|
|
}
|
|
|
|
context.tdb.commit ();
|
|
context.tdb.unlock ();
|
|
|
|
std::stringstream out;
|
|
out << "Imported "
|
|
<< (lines.size () - failed.size () - 1)
|
|
<< " tasks successfully, with "
|
|
<< failed.size ()
|
|
<< " errors."
|
|
<< std::endl;
|
|
|
|
if (failed.size ())
|
|
{
|
|
std::string bad;
|
|
join (bad, "\n", failed);
|
|
return out.str () + "\nCould not import:\n\n" + bad;
|
|
}
|
|
|
|
return out.str ();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static std::string importTask_1_6_0 (const std::vector <std::string>& lines)
|
|
{
|
|
std::vector <std::string> failed;
|
|
|
|
context.tdb.lock (context.config.get ("locking", true));
|
|
|
|
std::vector <std::string>::const_iterator it;
|
|
for (it = lines.begin (); it != lines.end (); ++it)
|
|
{
|
|
try
|
|
{
|
|
// Skip the first line, if it is a columns header line.
|
|
if (it->substr (0, 7) == "'uuid',")
|
|
continue;
|
|
|
|
std::vector <std::string> fields;
|
|
split (fields, *it, ',');
|
|
|
|
// If there is an unexpected number of fields, something is wrong. Perhaps
|
|
// an embedded comma, in which case there are (at least) two fields that
|
|
// need to be concatenated.
|
|
if (fields.size () > 13)
|
|
{
|
|
int safety = 10; // Shouldn't be more than 10 commas in a description/project.
|
|
|
|
do
|
|
{
|
|
std::vector <std::string> modified;
|
|
for (unsigned int f = 0; f < fields.size (); ++f)
|
|
{
|
|
if (fields[f][0] != '\'' &&
|
|
fields[f][fields[f].length () - 1] == '\'')
|
|
{
|
|
modified[modified.size () - 1] += "," + fields[f];
|
|
}
|
|
|
|
else
|
|
modified.push_back (fields[f]);
|
|
}
|
|
fields = modified;
|
|
|
|
if (safety-- <= 0)
|
|
throw "unrecoverable";
|
|
}
|
|
while (fields.size () > 13);
|
|
}
|
|
|
|
if (fields.size () < 13)
|
|
throw "unrecoverable";
|
|
|
|
// Build up this task ready for insertion.
|
|
Task task;
|
|
|
|
// Handle the 13 fields.
|
|
for (unsigned int f = 0; f < fields.size (); ++f)
|
|
{
|
|
switch (f)
|
|
{
|
|
case 0: // 'uuid'
|
|
task.set ("uuid", fields[f].substr (1, 36));
|
|
break;
|
|
|
|
case 1: // 'status'
|
|
if (fields[f] == "'pending'") task.setStatus (Task::pending);
|
|
else if (fields[f] == "'recurring'") task.setStatus (Task::recurring);
|
|
else if (fields[f] == "'deleted'") task.setStatus (Task::deleted);
|
|
else if (fields[f] == "'completed'") task.setStatus (Task::completed);
|
|
else if (fields[f] == "'waiting'") task.setStatus (Task::waiting);
|
|
break;
|
|
|
|
case 2: // 'tags'
|
|
if (fields[f].length () > 2)
|
|
{
|
|
std::string tokens = fields[f].substr (1, fields[f].length () - 2);
|
|
std::vector <std::string> tags;
|
|
split (tags, tokens, ' ');
|
|
for (unsigned int i = 0; i < tags.size (); ++i)
|
|
task.addTag (tags[i]);
|
|
}
|
|
break;
|
|
|
|
case 3: // entry
|
|
task.set ("entry", fields[f]);
|
|
break;
|
|
|
|
case 4: // start
|
|
if (fields[f].length ())
|
|
task.set ("start", fields[f]);
|
|
break;
|
|
|
|
case 5: // due
|
|
if (fields[f].length ())
|
|
task.set ("due", fields[f]);
|
|
break;
|
|
|
|
case 6: // recur
|
|
if (fields[f].length ())
|
|
task.set ("recur", fields[f]);
|
|
break;
|
|
|
|
case 7: // end
|
|
if (fields[f].length ())
|
|
task.set ("end", fields[f]);
|
|
break;
|
|
|
|
case 8: // 'project'
|
|
if (fields[f].length () > 2)
|
|
task.set ("project", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 9: // 'priority'
|
|
if (fields[f].length () > 2)
|
|
task.set ("priority", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 10: // 'fg'
|
|
if (fields[f].length () > 2)
|
|
task.set ("fg", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 11: // 'bg'
|
|
if (fields[f].length () > 2)
|
|
task.set ("bg", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
|
|
case 12: // 'description'
|
|
if (fields[f].length () > 2)
|
|
task.set ("description", fields[f].substr (1, fields[f].length () - 2));
|
|
break;
|
|
}
|
|
}
|
|
|
|
context.tdb.add (task);
|
|
}
|
|
|
|
catch (...)
|
|
{
|
|
failed.push_back (*it);
|
|
}
|
|
}
|
|
|
|
context.tdb.commit ();
|
|
context.tdb.unlock ();
|
|
|
|
std::stringstream out;
|
|
out << "Imported "
|
|
<< (lines.size () - failed.size () - 1)
|
|
<< " tasks successfully, with "
|
|
<< failed.size ()
|
|
<< " errors."
|
|
<< std::endl;
|
|
|
|
if (failed.size ())
|
|
{
|
|
std::string bad;
|
|
join (bad, "\n", failed);
|
|
return out.str () + "\nCould not import:\n\n" + bad;
|
|
}
|
|
|
|
return out.str ();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static std::string importTaskCmdLine (const std::vector <std::string>& lines)
|
|
{
|
|
std::vector <std::string> failed;
|
|
|
|
std::vector <std::string>::const_iterator it;
|
|
for (it = lines.begin (); it != lines.end (); ++it)
|
|
{
|
|
std::string line = *it;
|
|
|
|
try
|
|
{
|
|
context.args.clear ();
|
|
split (context.args, std::string ("add ") + line, ' ');
|
|
|
|
context.task.clear ();
|
|
context.cmd.command = "";
|
|
context.parse ();
|
|
handleAdd ();
|
|
context.clearMessages ();
|
|
}
|
|
|
|
catch (...)
|
|
{
|
|
context.clearMessages ();
|
|
failed.push_back (line);
|
|
}
|
|
}
|
|
|
|
std::stringstream out;
|
|
out << "Imported "
|
|
<< (lines.size () - failed.size ())
|
|
<< " tasks successfully, with "
|
|
<< failed.size ()
|
|
<< " errors."
|
|
<< std::endl;
|
|
|
|
if (failed.size ())
|
|
{
|
|
std::string bad;
|
|
join (bad, "\n", failed);
|
|
return out.str () + "\nCould not import:\n\n" + bad;
|
|
}
|
|
|
|
return out.str ();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static std::string importTodoSh_2_0 (const std::vector <std::string>& lines)
|
|
{
|
|
std::vector <std::string> failed;
|
|
|
|
context.tdb.lock (context.config.get ("locking", true));
|
|
|
|
std::vector <std::string>::const_iterator it;
|
|
for (it = lines.begin (); it != lines.end (); ++it)
|
|
{
|
|
try
|
|
{
|
|
context.args.clear ();
|
|
context.args.push_back ("add");
|
|
|
|
bool isPending = true;
|
|
Date endDate;
|
|
|
|
std::vector <std::string> words;
|
|
split (words, *it, ' ');
|
|
|
|
for (unsigned int w = 0; w < words.size (); ++w)
|
|
{
|
|
if (words[w].length () > 1 &&
|
|
words[w][0] == '+')
|
|
{
|
|
context.args.push_back (std::string ("project:") +
|
|
words[w].substr (1, std::string::npos));
|
|
}
|
|
|
|
// Convert "+aaa" to "project:aaa".
|
|
// Convert "@aaa" to "+aaa".
|
|
else if (words[w].length () > 1 &&
|
|
words[w][0] == '@')
|
|
{
|
|
context.args.push_back (std::string ("+") +
|
|
words[w].substr (1, std::string::npos));
|
|
}
|
|
|
|
// Convert "(A)" to "priority:H".
|
|
// Convert "(B)" to "priority:M".
|
|
// Convert "(?)" to "priority:L".
|
|
else if (words[w].length () == 3 &&
|
|
words[w][0] == '(' &&
|
|
words[w][2] == ')')
|
|
{
|
|
if (words[w][1] == 'A') context.args.push_back ("priority:H");
|
|
else if (words[w][1] == 'B') context.args.push_back ("priority:M");
|
|
else context.args.push_back ("priority:L");
|
|
}
|
|
|
|
// Set status, if completed.
|
|
else if (w == 0 &&
|
|
words[w] == "x")
|
|
{
|
|
isPending = false;
|
|
}
|
|
|
|
// Set status, and add an end date, if completed.
|
|
else if (! isPending &&
|
|
w == 1 &&
|
|
words[w].length () == 10 &&
|
|
words[w][4] == '-' &&
|
|
words[w][7] == '-')
|
|
{
|
|
endDate = Date (words[w], "Y-M-D");
|
|
}
|
|
|
|
// Just an ordinary word.
|
|
else
|
|
{
|
|
context.args.push_back (words[w]);
|
|
}
|
|
}
|
|
|
|
context.task.clear ();
|
|
context.cmd.command = "";
|
|
context.parse ();
|
|
decorateTask (context.task);
|
|
|
|
if (isPending)
|
|
{
|
|
context.task.setStatus (Task::pending);
|
|
}
|
|
else
|
|
{
|
|
context.task.setStatus (Task::completed);
|
|
|
|
char end[16];
|
|
sprintf (end, "%u", (unsigned int) endDate.toEpoch ());
|
|
context.task.set ("end", end);
|
|
}
|
|
|
|
context.tdb.add (context.task);
|
|
context.clearMessages ();
|
|
}
|
|
|
|
catch (...)
|
|
{
|
|
context.clearMessages ();
|
|
failed.push_back (*it);
|
|
}
|
|
}
|
|
|
|
context.tdb.commit ();
|
|
context.tdb.unlock ();
|
|
|
|
std::stringstream out;
|
|
out << "Imported "
|
|
<< (lines.size () - failed.size ())
|
|
<< " tasks successfully, with "
|
|
<< failed.size ()
|
|
<< " errors."
|
|
<< std::endl;
|
|
|
|
if (failed.size ())
|
|
{
|
|
std::string bad;
|
|
join (bad, "\n", failed);
|
|
return out.str () + "\nCould not import:\n\n" + bad;
|
|
}
|
|
return out.str ();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static std::string importText (const std::vector <std::string>& lines)
|
|
{
|
|
std::vector <std::string> failed;
|
|
int count = 0;
|
|
|
|
context.tdb.lock (context.config.get ("locking", true));
|
|
|
|
std::vector <std::string>::const_iterator it;
|
|
for (it = lines.begin (); it != lines.end (); ++it)
|
|
{
|
|
std::string line = *it;
|
|
|
|
// Strip comments
|
|
std::string::size_type pound = line.find ("#");
|
|
if (pound != std::string::npos)
|
|
line = line.substr (0, pound);
|
|
|
|
// Skip blank lines
|
|
if (line.length () > 0)
|
|
{
|
|
try
|
|
{
|
|
++count;
|
|
context.args.clear ();
|
|
split (context.args, std::string ("add ") + line, ' ');
|
|
|
|
context.task.clear ();
|
|
context.cmd.command = "";
|
|
context.parse ();
|
|
decorateTask (context.task);
|
|
|
|
context.tdb.add (context.task);
|
|
context.clearMessages ();
|
|
}
|
|
|
|
catch (...)
|
|
{
|
|
context.clearMessages ();
|
|
failed.push_back (line);
|
|
}
|
|
}
|
|
}
|
|
|
|
context.tdb.commit ();
|
|
context.tdb.unlock ();
|
|
|
|
std::stringstream out;
|
|
out << "Imported "
|
|
<< count
|
|
<< " tasks successfully, with "
|
|
<< failed.size ()
|
|
<< " errors."
|
|
<< std::endl;
|
|
|
|
if (failed.size ())
|
|
{
|
|
std::string bad;
|
|
join (bad, "\n", failed);
|
|
return out.str () + "\nCould not import:\n\n" + bad;
|
|
}
|
|
|
|
return out.str ();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static std::string importCSV (const std::vector <std::string>& lines)
|
|
{
|
|
std::vector <std::string> failed;
|
|
|
|
context.tdb.lock (context.config.get ("locking", true));
|
|
|
|
// Set up mappings. Assume no fields match.
|
|
std::map <std::string, int> mapping;
|
|
mapping ["id"] = -1;
|
|
mapping ["uuid"] = -1;
|
|
mapping ["status"] = -1;
|
|
mapping ["tags"] = -1;
|
|
mapping ["entry"] = -1;
|
|
mapping ["start"] = -1;
|
|
mapping ["due"] = -1;
|
|
mapping ["recur"] = -1;
|
|
mapping ["end"] = -1;
|
|
mapping ["project"] = -1;
|
|
mapping ["priority"] = -1;
|
|
mapping ["fg"] = -1;
|
|
mapping ["bg"] = -1;
|
|
mapping ["description"] = -1;
|
|
|
|
std::vector <std::string> headings;
|
|
split (headings, lines[0], ',');
|
|
|
|
for (unsigned int h = 0; h < headings.size (); ++h)
|
|
{
|
|
std::string name = lowerCase (trim (unquoteText (trim (headings[h]))));
|
|
|
|
// If there is a mapping for the field, use the value.
|
|
if (name == context.config.get ("import.synonym.id") ||
|
|
name == "id" ||
|
|
name == "#" ||
|
|
name == "sequence" ||
|
|
name.find ("num") != std::string::npos)
|
|
{
|
|
mapping["id"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.uuid") ||
|
|
name == "uuid" ||
|
|
name == "guid" ||
|
|
name.find ("unique") != std::string::npos)
|
|
{
|
|
mapping["uuid"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.status") ||
|
|
name == "status" ||
|
|
name == "condition" ||
|
|
name == "state")
|
|
{
|
|
mapping["status"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.tags") ||
|
|
name == "tags" ||
|
|
name.find ("categor") != std::string::npos ||
|
|
name.find ("tag") != std::string::npos)
|
|
{
|
|
mapping["tags"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.entry") ||
|
|
name == "entry" ||
|
|
name.find ("added") != std::string::npos ||
|
|
name.find ("created") != std::string::npos ||
|
|
name.find ("entered") != std::string::npos)
|
|
{
|
|
mapping["entry"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.start") ||
|
|
name == "start" ||
|
|
name.find ("began") != std::string::npos ||
|
|
name.find ("begun") != std::string::npos ||
|
|
name.find ("started") != std::string::npos)
|
|
{
|
|
mapping["start"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.due") ||
|
|
name == "due" ||
|
|
name.find ("expected") != std::string::npos)
|
|
{
|
|
mapping["due"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.recur") ||
|
|
name == "recur" ||
|
|
name == "frequency")
|
|
{
|
|
mapping["recur"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.end") ||
|
|
name == "end" ||
|
|
name == "done" ||
|
|
name.find ("complete") != std::string::npos)
|
|
{
|
|
mapping["end"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.project") ||
|
|
name == "project" ||
|
|
name.find ("proj") != std::string::npos)
|
|
{
|
|
mapping["project"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.priority") ||
|
|
name == "priority" ||
|
|
name == "pri" ||
|
|
name.find ("importan") != std::string::npos)
|
|
{
|
|
mapping["priority"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.fg") ||
|
|
name.find ("fg") != std::string::npos ||
|
|
name.find ("foreground") != std::string::npos ||
|
|
name.find ("color") != std::string::npos)
|
|
{
|
|
mapping["fg"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.bg") ||
|
|
name == "bg" ||
|
|
name.find ("background") != std::string::npos)
|
|
{
|
|
mapping["bg"] = (int)h;
|
|
}
|
|
|
|
else if (name == context.config.get ("import.synonym.description") ||
|
|
name.find ("desc") != std::string::npos ||
|
|
name.find ("detail") != std::string::npos ||
|
|
name.find ("task") != std::string::npos ||
|
|
name.find ("what") != std::string::npos)
|
|
{
|
|
mapping["description"] = (int)h;
|
|
}
|
|
}
|
|
|
|
// TODO Dump mappings and ask for confirmation?
|
|
|
|
std::vector <std::string>::const_iterator it = lines.begin ();
|
|
for (++it; it != lines.end (); ++it)
|
|
{
|
|
try
|
|
{
|
|
std::vector <std::string> fields;
|
|
split (fields, *it, ',');
|
|
|
|
Task task;
|
|
|
|
int f;
|
|
if ((f = mapping["uuid"]) != -1)
|
|
task.set ("uuid", lowerCase (unquoteText (trim (fields[f]))));
|
|
|
|
task.setStatus (Task::pending);
|
|
if ((f = mapping["status"]) != -1)
|
|
{
|
|
std::string value = lowerCase (unquoteText (trim (fields[f])));
|
|
|
|
if (value == "recurring") task.setStatus (Task::recurring);
|
|
else if (value == "deleted") task.setStatus (Task::deleted);
|
|
else if (value == "completed") task.setStatus (Task::completed);
|
|
else if (value == "waiting") task.setStatus (Task::waiting);
|
|
}
|
|
|
|
if ((f = mapping["tags"]) != -1)
|
|
{
|
|
std::string value = unquoteText (trim (fields[f]));
|
|
std::vector <std::string> tags;
|
|
split (tags, value, ' ');
|
|
for (unsigned int i = 0; i < tags.size (); ++i)
|
|
task.addTag (tags[i]);
|
|
}
|
|
|
|
if ((f = mapping["entry"]) != -1)
|
|
task.set ("entry", lowerCase (unquoteText (trim (fields[f]))));
|
|
|
|
if ((f = mapping["start"]) != -1)
|
|
task.set ("start", lowerCase (unquoteText (trim (fields[f]))));
|
|
|
|
if ((f = mapping["due"]) != -1)
|
|
task.set ("due", lowerCase (unquoteText (trim (fields[f]))));
|
|
|
|
if ((f = mapping["recur"]) != -1)
|
|
task.set ("recur", lowerCase (unquoteText (trim (fields[f]))));
|
|
|
|
if ((f = mapping["end"]) != -1)
|
|
task.set ("end", lowerCase (unquoteText (trim (fields[f]))));
|
|
|
|
if ((f = mapping["project"]) != -1)
|
|
task.set ("project", unquoteText (trim (fields[f])));
|
|
|
|
if ((f = mapping["priority"]) != -1)
|
|
{
|
|
std::string value = upperCase (unquoteText (trim (fields[f])));
|
|
if (value == "H" || value == "M" || value == "L")
|
|
task.set ("priority", value);
|
|
}
|
|
|
|
if ((f = mapping["fg"]) != -1)
|
|
task.set ("fg", lowerCase (unquoteText (trim (fields[f]))));
|
|
|
|
if ((f = mapping["bg"]) != -1)
|
|
task.set ("bg", lowerCase (unquoteText (trim (fields[f]))));
|
|
|
|
if ((f = mapping["description"]) != -1)
|
|
task.set ("description", unquoteText (trim (fields[f])));
|
|
|
|
context.tdb.add (task);
|
|
}
|
|
|
|
catch (...)
|
|
{
|
|
failed.push_back (*it);
|
|
}
|
|
}
|
|
|
|
context.tdb.commit ();
|
|
context.tdb.unlock ();
|
|
|
|
std::stringstream out;
|
|
out << "Imported "
|
|
<< (lines.size () - failed.size () - 1)
|
|
<< " tasks successfully, with "
|
|
<< failed.size ()
|
|
<< " errors."
|
|
<< std::endl;
|
|
|
|
if (failed.size ())
|
|
{
|
|
std::string bad;
|
|
join (bad, "\n", failed);
|
|
return out.str () + "\nCould not import:\n\n" + bad;
|
|
}
|
|
|
|
return out.str ();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
std::string handleImport ()
|
|
{
|
|
std::stringstream out;
|
|
|
|
// Use the description as a file name.
|
|
std::string file = trim (context.task.get ("description"));
|
|
if (file.length () > 0)
|
|
{
|
|
// Load the file.
|
|
std::vector <std::string> all;
|
|
slurp (file, all, true);
|
|
|
|
std::vector <std::string> lines;
|
|
std::vector <std::string>::iterator it;
|
|
for (it = all.begin (); it != all.end (); ++it)
|
|
{
|
|
std::string line = *it;
|
|
|
|
// Strip comments
|
|
std::string::size_type pound = line.find ("#");
|
|
if (pound != std::string::npos)
|
|
line = line.substr (0, pound);
|
|
|
|
trim (line);
|
|
|
|
// Skip blank lines
|
|
if (line.length () > 0)
|
|
lines.push_back (line);
|
|
}
|
|
|
|
// Take a guess at the file type.
|
|
fileType type = determineFileType (lines);
|
|
std::string identifier;
|
|
switch (type)
|
|
{
|
|
case task_1_4_3: identifier = "This looks like an older task export file."; break;
|
|
case task_1_5_0: identifier = "This looks like a recent task export file."; break;
|
|
case task_1_6_0: identifier = "This looks like a current task export file."; break;
|
|
case task_cmd_line: identifier = "This looks like task command line arguments."; break;
|
|
case todo_sh_2_0: identifier = "This looks like a todo.sh 2.x file."; break;
|
|
case csv: identifier = "This looks like a CSV file, but not a task export file."; break;
|
|
case text: identifier = "This looks like a text file with one task per line."; break;
|
|
case not_a_clue:
|
|
throw std::string ("Task cannot determine which type of file this is, "
|
|
"and cannot proceed.");
|
|
}
|
|
|
|
// For tty users, confirm the import, as it is destructive.
|
|
if (isatty (fileno (stdout)))
|
|
if (! confirm (identifier + " Okay to proceed?"))
|
|
throw std::string ("Task will not import any data.");
|
|
|
|
// Determine which type it might be, then attempt an import.
|
|
switch (type)
|
|
{
|
|
case task_1_4_3: out << importTask_1_4_3 (lines); break;
|
|
case task_1_5_0: out << importTask_1_5_0 (lines); break;
|
|
case task_1_6_0: out << importTask_1_6_0 (lines); break;
|
|
case task_cmd_line: out << importTaskCmdLine (lines); break;
|
|
case todo_sh_2_0: out << importTodoSh_2_0 (lines); break;
|
|
case csv: out << importCSV (lines); break;
|
|
case text: out << importText (lines); break;
|
|
case not_a_clue: /* to stop the compiler from complaining. */ break;
|
|
}
|
|
}
|
|
else
|
|
throw std::string ("You must specify a file to import.");
|
|
|
|
return out.str ();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|