1 
2 #include "todotxt.h"
3 
4 // Todo.txt file format: https://github.com/ginatrapani/todo.txt-cli/wiki/The-Todo.txt-Format
5 
6 #include <QFile>
7 #include <QTextStream>
8 #include <QStringList>
9 #include <QDate>
10 #include <set>
11 #include <QSettings>
12 #include <QRegularExpression>
13 #include <QDebug>
14 #include <QUuid>
15 #include <QDir>
16 #include "def.h"
17 
todotxt()18 todotxt::todotxt()
19 {
20     // This is part of the old implementation where we'd have a constant undoDir that could
21     // be shared by many applications.  I leave it here in case we end up with using too much storage
22     // and need manual cleanups.
23     //cleanupUndoDir();
24 
25     undoDir = new QTemporaryDir();
26     if(!undoDir->isValid()){
27         qDebug()<<"Could not create undo dir"<<Qt::endl;
28     } else {
29         qDebug()<<"Created undo dir at "<<undoDir->path()<<Qt::endl;
30     }
31 }
32 
~todotxt()33 todotxt::~todotxt()
34 {
35     if(undoDir)
36         delete undoDir;
37 }
38 
setdirectory(QString & dir)39 void todotxt::setdirectory(QString &dir){
40     filedirectory=dir;
41 }
42 
43 static QRegularExpression regex_project("\\s(\\+[^\\s]+)");
44 static QRegularExpression regex_context("\\s(\\@[^\\s]+)");
45 
parse()46 void todotxt::parse(){
47 
48     // Before we do anything here, we make sure we have covered our bases with an undo save
49     // Note, that except for the first read, if we end up doing a save here, something has changed on disk
50     // outside of this program.
51     saveToUndo();
52 
53     QSettings settings;
54     //qDebug()<<"todotxt::parse";
55     // parse the files todo.txt and done.txt (for now only todo.txt)
56     todo.clear();
57     active_contexts.clear();
58     active_projects.clear();
59     QString todofile=getTodoFilePath();
60 
61     slurp(todofile,todo);
62 
63     if(settings.value(SETTINGS_SHOW_ALL,DEFAULT_SHOW_ALL).toBool()){
64         // Donefile as well
65         QString donefile = getDoneFilePath();
66         slurp(donefile,todo);
67     }
68 
69       if(settings.value(SETTINGS_THRESHOLD_LABELS).toBool()){
70           // Get all active tags with either a @ or a + sign infront of them, as they can be used for thresholds
71           foreach (auto line, todo) {
72               if(line.startsWith("x ")){
73                   continue; // Inactive so we don't care
74               } else {
75                 auto matches = regex_project.globalMatch(line);
76                 while(matches.hasNext()){
77                     auto match = matches.next();
78                     auto project = match.captured(1);
79                     active_projects.insert(project);
80                 }
81                 matches = regex_context.globalMatch(line);
82                 while(matches.hasNext()){
83                     auto match = matches.next();
84                     active_contexts.insert(match.captured(1));
85                 }
86               }
87 
88           }
89       }
90 }
91 
getTodoFilePath()92 QString todotxt::getTodoFilePath(){
93     QSettings settings;
94     QString dir = settings.value(SETTINGS_DIRECTORY).toString();
95     QString todofile = dir.append(TODOFILE);
96     return todofile;
97 }
98 
99 
getDoneFilePath()100 QString todotxt::getDoneFilePath(){
101     QSettings settings;
102     QString dir = settings.value(SETTINGS_DIRECTORY).toString();
103     QString todofile = dir.append(DONEFILE);
104     return todofile;
105 }
106 
getDeletedFilePath()107 QString todotxt::getDeletedFilePath(){
108     QSettings settings;
109     QString dir = settings.value(SETTINGS_DIRECTORY).toString();
110     QString todofile = dir.append(DELETEDFILE);
111     return todofile;
112 }
113 
getActive(QString & filter,vector<QString> & output)114 void todotxt::getActive(QString& filter,vector<QString> &output){
115         // Obsolete... remove?
116     Q_UNUSED(filter);
117         for(vector<QString>::iterator iter=todo.begin();iter!=todo.end();iter++){
118                 if( (*iter).length()==0 || (*iter).at(0) == 'x')
119                         continue;
120                 output.push_back( (*iter));
121         }
122 }
123 
124 
isInactive(QString & text)125 bool todotxt::isInactive(QString &text){
126     QSettings settings;
127     QString t=settings.value(SETTINGS_INACTIVE).toString();
128     if(t.isEmpty())
129         return false;
130     QStringList inactives = t.split(";");
131     for(int i=0;i<inactives.count();i++){
132         if(text.contains(inactives[i])){
133             return true;
134         }
135     }
136 
137     if(settings.value(SETTINGS_THRESHOLD_INACTIVE).toBool()){
138         return threshold_hide(text);
139     }
140 
141     return false;
142 }
143 
144 /* Comparator function.. We need to remove all the junk in the beginning of the line */
lessThan(QString & s1,QString & s2)145 bool todotxt::lessThan(QString &s1,QString &s2){
146     return prettyPrint(s1).toLower() < prettyPrint(s2).toLower();
147 }
148 
149 static QRegularExpression regex_threshold_date("t:(\\d\\d\\d\\d-\\d\\d-\\d\\d)");
150 static QRegularExpression regex_threshold_project("t:(\\+[^\\s]+)");
151 static QRegularExpression regex_threshold_context("t:(\\@[^\\s]+)");
152 static QRegularExpression regex_due_date("due:(\\d\\d\\d\\d-\\d\\d-\\d\\d)");
153 
threshold_hide(QString & t)154 bool todotxt::threshold_hide(QString &t){
155     QSettings settings;
156     if(settings.value(SETTINGS_THRESHOLD).toBool()){
157         auto matches=regex_threshold_date.globalMatch(t);
158         while(matches.hasNext()){
159             QString today = getToday();
160             QString td = matches.next().captured(1);
161             if(td.compare(today)>0){
162                 return true; // Don't show this one since it's in the future
163             }
164 
165         }
166     }
167 
168 
169     if(settings.value(SETTINGS_THRESHOLD_LABELS).toBool()){
170         auto matches=regex_threshold_project.globalMatch(t);
171         while(matches.hasNext()){
172             QString project = matches.next().captured(1);
173             if(active_projects.count(project)==1)
174                 return true; // There is an active project with this name, so we skip this
175         }
176 
177         matches=regex_threshold_context.globalMatch(t);
178         while(matches.hasNext()){
179             QString project = matches.next().captured(1);
180             if(active_contexts.count(project)==1)
181                 return true; // There is an active project with this name, so we skip this
182         }
183     }
184     return false;
185 }
186 
187 
getAll(QString & filter,vector<QString> & output)188 void todotxt::getAll(QString& filter,vector<QString> &output){
189         // Vectors are probably not the best here...
190     Q_UNUSED(filter);
191         set<QString> prio;
192         vector<QString> open;
193         vector<QString> done;
194         vector<QString> inactive;
195         QSettings settings;
196         QString t=settings.value(SETTINGS_INACTIVE).toString();
197         QStringList inactives = t.split(";");
198         if(!t.contains(";")){
199             // There is really nothing here but inactives will still have one item. Lets just remove it
200             inactives.clear();
201         }
202 
203         bool separateinactives = settings.value(SETTINGS_SEPARATE_INACTIVES).toBool();
204 
205         for(vector<QString>::iterator iter=todo.begin();iter!=todo.end();iter++){
206             QString line = (*iter); // For debugging
207             if(line.isEmpty())
208                 continue;
209 
210             // Begin by checking for inactive, as there are two different ways of sorting those
211             bool inact=false;
212             for(int i=0;i<inactives.count();i++){
213                 if((*iter).contains(inactives[i])){
214                     inact=true;
215                     break;
216 
217                 }
218             }
219 
220             // If we are respecting thresholds, we should check for that
221             bool no_show_threshold = threshold_hide((*iter));
222 
223 
224             if(no_show_threshold){
225                 if(settings.value(SETTINGS_THRESHOLD_INACTIVE).toBool()){
226                     inact=true;
227                 } else {
228                     continue;
229                 }
230             }
231 
232 
233             if(!(inact&&separateinactives) && (*iter).at(0) == '(' && (*iter).at(2) == ')'){
234                 prio.insert((*iter));
235             } else if ( (*iter).at(0) == 'x'){
236                 done.push_back((*iter));
237             } else if(inact){
238                 inactive.push_back((*iter));
239             }
240             else {
241                 open.push_back((*iter));
242             }
243         }
244 
245         // Sort the open and done sections alphabetically if needed
246 
247         if(settings.value(SETTINGS_SORT_ALPHA).toBool()){
248             std::sort(open.begin(),open.end(),lessThan);
249             std::sort(inactive.begin(),inactive.end(),lessThan);
250             std::sort(done.begin(),done.end(),lessThan);
251         }
252 
253         for(set<QString>::iterator iter=prio.begin();iter!=prio.end();iter++)
254             output.push_back((*iter));
255         for(vector<QString>::iterator iter=open.begin();iter!=open.end();iter++)
256             output.push_back((*iter));
257         for(vector<QString>::iterator iter=inactive.begin();iter!=inactive.end();iter++)
258             output.push_back((*iter));
259         for(vector<QString>::iterator iter=done.begin();iter!=done.end();iter++)
260             output.push_back((*iter));
261 }
262 
getState(QString & row)263 Qt::CheckState todotxt::getState(QString& row){
264     if(row.length()>1 && row.at(0)=='x' && row.at(1)==' '){
265         return Qt::Checked;
266     } else {
267         return Qt::Unchecked;
268     }
269 }
270 
getToday()271 QString todotxt::getToday(){
272     QDate d = QDate::currentDate();
273     return d.toString("yyyy-MM-dd");
274 }
275 
getRelativeDate(QString shortform,QDate d)276 QString todotxt::getRelativeDate(QString shortform,QDate d){
277     QString extra = "";
278     // The short form supported for now is +\\dd
279     QRegularExpression reldateregex("\\+(\\d+)([dwmypb])");
280     QRegularExpressionMatch m = reldateregex.match(shortform);
281     if(m.hasMatch()){
282         if(m.captured(2).contains('d')){
283             d= d.addDays(m.captured(1).toInt());
284         } else if(m.captured(2).contains('w')){
285             d= d.addDays(m.captured(1).toInt()*7);
286         } else if(m.captured(2).contains('m')){
287             d= d.addMonths(m.captured(1).toInt());
288         } else if(m.captured(2).contains('y')) {
289             d= d.addYears(m.captured(1).toInt());
290         } else if(m.captured(2).contains('p')){
291             // Ok. This is the procrastination 'feature'. Add a random number of days and also say that this was procrastrinated
292             d = d.addDays(rand()%m.captured(1).toInt()+1);
293             //extra = " +procrastinated";
294         } else if (m.captured(2).contains('b')){
295             // Business days. Naive implementation
296             // 1=Monday, 6=Sat, 7=sun
297             int days=0;
298             int addDays = m.captured(1).toInt();
299             while(days<addDays){
300                 d = d.addDays(1); // add one at a time
301                 if(d.dayOfWeek() <6){
302                     days++;
303                 }
304             }
305 
306         }
307         return d.toString("yyyy-MM-dd")+extra;
308     } else {
309         return shortform; // Don't know what to do.. so just put the string back
310     }
311 }
312 
313 
restoreFiles(QString namePrefix)314 void todotxt::restoreFiles(QString namePrefix){
315     qDebug()<<"Restoring: "<<namePrefix<<Qt::endl;
316     qDebug()<<"Pointer: "<<undoPointer<<Qt::endl;
317     // Copy back files from the undo
318     QString newtodo = namePrefix+TODOFILE;
319     QString newdone = namePrefix+DONEFILE;
320     QString newdeleted = namePrefix+DELETEDFILE;
321 
322     if(QFile::exists(newtodo)){
323         QFile::remove(getTodoFilePath());
324         QFile::copy(newtodo,getTodoFilePath());
325     }
326     if(QFile::exists(newdone)){
327         QFile::remove(getDoneFilePath());
328         QFile::copy(newdone,getDoneFilePath());
329     }
330     if(QFile::exists(newdeleted)){
331         QFile::remove(getDeletedFilePath());
332         QFile::copy(newdeleted,getDeletedFilePath());
333     }
334 
335 }
undo()336 bool todotxt::undo()
337 {
338     // Check if we can
339     if((int) undoBuffer.size()>undoPointer+1){
340         // yep. there is more in the buffer.
341         // Ok. Here it is obvious that I should have implemented undoBuffer as a vector and probably have the pointer to be a negative index
342         undoPointer++;
343         restoreFiles(undoBuffer[undoBuffer.size()-(1+undoPointer)]);
344         return true;
345     }
346     return false;
347 }
348 
redo()349 bool todotxt::redo()
350 {
351     // Check if we can
352     if(undoPointer>0){
353         // yep. there is more in the buffer.
354         // Ok. Here it is obvious that I should have implemented undoBuffer as a vector and probably have the pointer to be a negative index
355         undoPointer--;
356         restoreFiles(undoBuffer[undoBuffer.size()-(1+undoPointer)]);
357         return true;
358     }
359     return false;
360 }
361 
undoPossible()362 bool todotxt::undoPossible()
363 {
364     if((int) undoBuffer.size()>undoPointer+1){
365         return true;
366     }
367     return false;
368 }
369 
redoPossible()370 bool todotxt::redoPossible()
371 {
372     if(undoPointer>0){
373         return true;
374     }
375     return false;
376 }
377 
getUndoDir()378 QString todotxt::getUndoDir()
379 {
380     if(undoDir->isValid()){
381         return undoDir->path()+"/";
382     }
383 
384     // The below code is backup for undo directory created in the same folder
385     // in case there was a problem with getting a temp directory (shouldn't happen.. but)
386     QSettings settings;
387     QString uuid = settings.value(SETTINGS_UUID,DEFAULT_UUID).toString();
388     QString dirbase = settings.value(SETTINGS_DIRECTORY).toString();
389     QString dir = dirbase+".todour_undo_"+uuid+"/";
390     // Check that the dir exists
391     QDir directory = QDir(dir);
392     if(!directory.exists()){
393         directory.mkdir(dir);
394     }
395     return dir;
396 }
397 
getNewUndoNameDirAndPrefix()398 QString todotxt::getNewUndoNameDirAndPrefix()
399 {
400     // Make a file path that is made to have _todo.txt or _done.txt appended to it
401     // We want the name to be anything that doesn't crash with any other. This can be linear numbering or just an UUID
402     // For now it's UUID but a linear numbering (with a solution to multiple clients running with the same undo-directory) would make
403     // more sense and be easier to follow for end-users in case they are looking through the files.
404     return getUndoDir()+QUuid::createUuid().toString()+"_";
405 }
406 
cleanupUndoDir()407 void todotxt::cleanupUndoDir()
408 {
409     // Go through the directory and remove everything that is older than 14 days old.
410     // Why 14 days? Because. That's why :)
411     // (I simply don't think this is interesting to have configurable.. But simple enough to do in case needed)
412     QDir directory(getUndoDir());
413     QStringList files = directory.entryList(QDir::Files);
414     QDateTime expirationTime = QDateTime::currentDateTime();
415     expirationTime = expirationTime.addDays(-14);
416     qDebug()<<"Checking for undo files to cleanup..."<<Qt::endl;
417     foreach(QString filename, files) {
418         auto finfo = QFileInfo(directory.filePath(filename));
419         QDateTime created = finfo.birthTime();
420         if(expirationTime.daysTo(created)<0 || !created.isValid()){
421             //qDebug()<<"We should remove file (but wont now)"<<filename<<Qt::endl;
422             if(!QFile::remove(directory.filePath(filename))){
423                 qDebug()<<"Failed to remove: "<<filename<<Qt::endl;
424             }
425         }
426     }
427 }
428 
429 // Returns true of there is a need for a new undo
checkNeedForUndo()430 bool todotxt::checkNeedForUndo(){
431     // check if the todo.txt is any different from the lastUndo file
432     vector<QString> todo;
433     vector<QString> lastUndo;
434 
435     if(undoBuffer.empty()){
436         return true;
437     }
438 
439     QString todofile = getTodoFilePath();
440     QString undofile = undoBuffer.back()+(TODOFILE);
441     slurp(todofile,todo);
442     slurp(undofile,lastUndo);
443     if(todo.size()!=lastUndo.size()){
444         qDebug()<<"Sizes differ: "<<todofile<<" vs "<<undofile<<Qt::endl;
445         return true;
446     }
447 
448     // We got this far, we have to go trhough the files line by line
449     for(int i=0;i<(int) todo.size();i++){
450         if(todo[i] != lastUndo[i]){
451             qDebug()<<todo[i]<<" != "<<lastUndo[i]<<Qt::endl;
452             return true;
453         }
454     }
455 
456     // Files are the same. No need to save a new undo.
457     return false;
458 }
459 
saveToUndo()460 void todotxt::saveToUndo()
461 {
462     // This should be called every time we will read todo.txt
463 
464     // if we're moving around the undoBuffer, we should not be doing anything
465     if(undoPointer)
466         return;
467 
468     // Start with checking if there is a change in the file compared to the last one in the undoBuffer
469     // (or if the undoBuffer is empty)
470     if(checkNeedForUndo() ){
471         // Creating a new undo is pretty simple.
472         // Just copy the todo.txt and the done.txt to the undo directory under a new name and save the filename in the undoBuffer
473         QString namePrefix = getNewUndoNameDirAndPrefix();
474         QString newtodo = namePrefix+TODOFILE;
475         QString newdone = namePrefix+DONEFILE;
476         QString newdeleted = namePrefix+DELETEDFILE;
477 
478         QFile::copy(getTodoFilePath(),newtodo);
479         QFile::copy(getDoneFilePath(),newdone);
480         QFile::copy(getDeletedFilePath(),newdeleted);
481 
482         undoBuffer.push_back(namePrefix);
483         qDebug()<<"Added to undoBuffer: "<<namePrefix<<Qt::endl;
484         qDebug()<<"Buffer is now: "<<undoBuffer.size()<<Qt::endl;
485     }
486 
487 }
488 
prettyPrint(QString & row,bool forEdit)489 QString todotxt::prettyPrint(QString& row,bool forEdit){
490     QString ret;
491     QSettings settings;
492 
493     // Remove dates
494     todoline tl;
495     String2Todo(row,tl);
496 
497     ret = tl.priority;
498     if(forEdit || settings.value(SETTINGS_SHOW_DATES).toBool()){
499         ret.append(tl.closedDate+tl.createdDate);
500     }
501 
502     ret.append(tl.text);
503 
504     // Remove all leading and trailing spaces
505     return ret.trimmed();
506 }
507 
slurp(QString & filename,vector<QString> & content)508 void todotxt::slurp(QString& filename,vector<QString>& content){
509     QSettings settings;
510     QFile file(filename);
511     if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
512         return;
513 
514     QTextStream in(&file);
515     in.setCodec("UTF-8");
516     while (!in.atEnd()) {
517         QString line = in.readLine();
518         if(settings.value(SETTINGS_REMOVE_DOUBLETS,DEFAULT_REMOVE_DOUBLETS).toBool()){
519             // This can be optimized by for example using a set<QString>
520             if(std::find(content.begin(),content.end(),line) != content.end()){
521                 // We found this line. So we ignore it
522                 continue;
523             }
524         }
525         content.push_back(line);
526      }
527 }
528 
write(QString & filename,vector<QString> & content)529 void todotxt::write(QString& filename,vector<QString>&  content){
530     // As we're about to write a change to the file, we have to consider what is now in the file as valid
531     // Thus we point the undo pointer to the last entry and check if we need to save what is now in the files before we overwrite it
532     undoPointer=0;
533     saveToUndo();
534 
535     //qDebug()<<"todotxt::write("<<filename<<")";
536     QFile file(filename);
537     if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
538          return;
539 
540         QTextStream out(&file);
541         out.setCodec("UTF-8");
542         for(unsigned int i = 0;	i<content.size(); i++)
543             out << content.at(i) << "\n";
544 
545 
546 }
547 
remove(QString line)548 void todotxt::remove(QString line){
549     // Remove the line, but perhaps saving it for later as well..
550     QSettings settings;
551     if(settings.value(SETTINGS_DELETED_FILE).toBool()){
552         QString deletedfile = getDeletedFilePath();
553         vector<QString> deleteddata;
554         slurp(deletedfile,deleteddata);
555         deleteddata.push_back(line);
556         write(deletedfile,deleteddata);
557     }
558     QString tmp;
559     update(line,false,tmp);
560 }
561 
562 
archive()563 void todotxt::archive(){
564     // Slurp the files
565     QSettings settings;
566     QString todofile = getTodoFilePath();
567     QString donefile = getDoneFilePath();
568     vector<QString> tododata;
569     vector<QString> donedata;
570     slurp(todofile,tododata);
571     slurp(donefile,donedata);
572     for(vector<QString>::iterator iter=tododata.begin();iter!=tododata.end();){
573         if((*iter).length()>0 && (*iter).at(0)=='x'){
574             donedata.push_back((*iter));
575             iter=tododata.erase(iter);
576         } else {
577             // No change
578             iter++;
579         }
580 
581     }
582     write(todofile,tododata);
583     write(donefile,donedata);
584     parse();
585 }
586 
refresh()587 void todotxt::refresh(){
588     parse();
589 }
590 
update(QString & row,bool checked,QString & newrow)591 void todotxt::update(QString &row, bool checked, QString &newrow){
592     // First slurp the file.
593     QSettings settings;
594     QString todofile = getTodoFilePath();
595     vector<QString> data;
596     slurp(todofile,data);
597     QString additional_item = ""; // This is for recurrence. If there is a new item created, put it here since we have to add it after the file is written
598 
599     // Preprocessing of the line
600     if(settings.value(SETTINGS_THRESHOLD).toBool()){
601         QRegularExpression threshold_shorthand("(t:\\+\\d+[dwmypb])");
602         QRegularExpressionMatch m = threshold_shorthand.match(newrow);
603         if(m.hasMatch()){
604             newrow = newrow.replace(m.captured(1),"t:"+getRelativeDate(m.captured(1).mid(2)));
605         }
606     }
607 
608     if(settings.value(SETTINGS_DUE).toBool()){
609         QRegularExpression due_shorthand("(due:\\+\\d+[dwmypb])");
610         QRegularExpressionMatch m = due_shorthand.match(newrow);
611         if(m.hasMatch()){
612             newrow = newrow.replace(m.captured(1),"due:"+getRelativeDate(m.captured(1).mid(2)));
613         }
614     }
615 
616     if(row.isEmpty()){
617         todoline tl;
618         String2Todo(newrow,tl);
619         // Add a date to the line if where doing dates
620         if(settings.value(SETTINGS_DATES).toBool()){
621             QString today = getToday()+" ";
622             tl.createdDate = today;
623         }
624 
625         // Just add the line
626         data.push_back(Todo2String(tl));
627 
628     } else {
629         for(vector<QString>::iterator iter=data.begin();iter!=data.end();iter++){
630             QString *r = &(*iter);
631             if(!r->compare(row)){
632                 // Here it is.. Lets modify if we shouldn't remove it alltogether
633                 if(newrow.isEmpty()){
634                     // Remove it
635                     iter=data.erase(iter);
636                     break;
637                 }
638 
639                 if(checked && !r->startsWith("x ")){
640                     todoline tl;
641                     String2Todo(*r,tl);
642                     tl.checked=true;
643 
644                     QString date;
645                     if(settings.value(SETTINGS_DATES).toBool()){
646                             date.append(getToday()+" "); // Add a date if needed
647                     }
648                     tl.closedDate=date;
649 
650                     // Handle recurrence
651                     //QRegularExpression rec_normal("(rec:\\d+[dwmyb])");
652                     QRegularExpression rec("(rec:\\+?\\d+[dwmybp])");
653 
654                     // Get the "addition" from rec
655                     QRegularExpressionMatch m = rec.match(tl.text);
656                     if(m.hasMatch()){
657                         // Figure out what date we should use
658                         bool isStrict = true;
659                         QString rec_add = m.captured(1).mid(4);
660                         // Add a '+' if it's not there due to how getRelativeDate works
661                         if(rec_add.at(0)!= '+'){
662                             rec_add.insert(0,'+');
663                             isStrict = false;
664                         }
665 
666 
667 
668                         // Make a copy. It's time to start altering that one
669                         if(!tl.priority.isEmpty()){
670                             additional_item = tl.priority+tl.text;
671                         } else {
672                             additional_item = tl.text;
673                         }
674 
675                         // Handle the special case that we're in a rec but there is neither a due nor t
676                         if(!regex_threshold_date.match(tl.text).hasMatch() && !regex_due_date.match(tl.text).hasMatch()){
677                             // The rec doesn't have any real point without having a t or a due.
678                             // Add one with todays date
679                             additional_item.append(" "+settings.value(SETTINGS_DEFAULT_THRESHOLD,DEFAULT_DEFAULT_THRESHOLD).toString()+getToday());
680                         }
681 
682 
683                         // Get the t:
684                         auto mt = regex_threshold_date.globalMatch(additional_item);
685                         while(mt.hasNext()){
686                             QString old_t = mt.next().captured(1);
687                             QString newdate = isStrict?getRelativeDate(rec_add, QDate::fromString(old_t,"yyyy-MM-dd")):getRelativeDate(rec_add);
688                             additional_item.replace("t:"+old_t,"t:"+newdate);
689                         }
690                         // Get the due:
691                         auto md = regex_due_date.match(additional_item);
692                         if(md.hasMatch()){
693                             QString old_due = md.captured(1);
694                             QString newdate = isStrict?getRelativeDate(rec_add, QDate::fromString(old_due,"yyyy-MM-dd")):getRelativeDate(rec_add);
695                             additional_item.replace("due:"+old_due,"due:"+newdate);
696                         }
697                     }
698 
699 
700                     *r=Todo2String(tl);
701 
702                 }
703                 else if(!checked && r->startsWith("x ")){
704                     todoline tl;
705                     String2Todo(*r,tl);
706                     tl.checked=false;
707                     tl.closedDate="";
708                     *r=Todo2String(tl);
709                 } else {
710                     todoline tl;
711                     String2Todo(row,tl);
712                     todoline newtl;
713                     String2Todo(newrow,newtl);
714                     tl.priority=newtl.priority;
715                     tl.text=newtl.text;
716                     tl.createdDate = newtl.createdDate;
717                     tl.closedDate = newtl.closedDate;
718                     *r = Todo2String(tl);
719                 }
720                 break;
721             }
722         }
723     }
724 
725     write(todofile,data);
726     if(!additional_item.isEmpty()){
727         QString empty="";
728         this->update(empty,false,additional_item);
729     }
730     parse();
731 }
732 
733 // A todo.txt line looks like this
734 static QRegularExpression todo_line("(x\\s+)?(\\([A-Z]\\)\\s+)?(\\d\\d\\d\\d-\\d\\d-\\d\\d\\s+)?(\\d\\d\\d\\d-\\d\\d-\\d\\d\\s+)?(.*)");
735 
String2Todo(QString & line,todoline & t)736 void todotxt::String2Todo(QString &line,todoline &t){
737     QRegularExpressionMatch match = todo_line.match(line);
738     if(match.hasMatch() && match.lastCapturedIndex()==5){
739 
740         if(match.captured(1).isEmpty()){
741             t.checked=false;
742         } else {
743             t.checked=true;
744         }
745 
746         t.priority=match.captured(2);
747         if(t.checked){
748             t.closedDate=match.captured(3);
749             t.createdDate=match.captured(4);
750         } else {
751             t.createdDate=match.captured(3); // No closed date on a line that isn't closed.
752         }
753         t.text = match.captured(5);
754 
755 
756     } else {
757         t.checked=false;
758         t.priority="";
759         t.closedDate="";
760         t.createdDate="";
761         t.text="";
762     }
763 
764 }
765 
Todo2String(todoline & t)766 QString todotxt::Todo2String(todoline &t){
767     QString ret;
768     QSettings settings;
769 
770     // Yep, an ugly side effect, but it make sure we're having the right format all the time
771     if(t.checked && t.createdDate.isEmpty()){
772         t.createdDate = t.closedDate;
773     }
774 
775     if(t.checked){
776         ret.append("x ");
777     } else {
778         // Priority shall only be written if we are active
779         ret.append(t.priority);
780     }
781     ret.append(t.closedDate);
782     ret.append(t.createdDate);
783     // Here we have to decide how to handle priority tag if we have one
784     if(t.checked && !t.priority.isEmpty()){
785         prio_on_close how = (prio_on_close) settings.value(SETTINGS_PRIO_ON_CLOSE,DEFAULT_PRIO_ON_CLOSE).toInt();
786         switch(how){
787             case prio_on_close::removeit:
788                 break; // We do nothing. Just forget it exists
789             case prio_on_close::moveit:
790                 ret.append(t.priority+" "); // Put the priority first in the text
791                 break;
792             case prio_on_close::tagit:
793                 if(t.priority.size()>2){
794                     ret.append("pri:");
795                     ret.append(t.priority.at(1));
796                     ret.append(" ");
797                 }
798                 break;
799         }
800     }
801 
802     ret.append(t.text);
803     return ret;
804 }
805 
806 // Check when this is due
807 
808 
dueIn(QString & text)809 int todotxt::dueIn(QString &text){
810     int ret=INT_MAX;
811     QSettings settings;
812     if(settings.value(SETTINGS_DUE).toBool()){
813         QRegularExpressionMatch m=regex_due_date.match(text);
814         if(m.hasMatch()){
815             QString ds = m.captured(1);
816             QDate d = QDate::fromString(ds,"yyyy-MM-dd");
817             return (int) QDate::currentDate().daysTo(d);
818         }
819     }
820     return ret;
821 }
822 
823 //QRegularExpression regex_url("[a-zA-Z0-9_]+://[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=\\(\\)]*)");
824 static QRegularExpression regex_url("[a-zA-Z0-9_]+:\\/\\/([-a-zA-Z0-9@:%_\\+.~#?&\\/=\\(\\)\\{\\}\\\\]*)");
825 
getURL(QString & line)826 QString todotxt::getURL(QString &line){
827     QRegularExpressionMatch m=regex_url.match(line);
828     if(m.hasMatch()){
829         //qDebug()<<"URL: "<<m.capturedTexts().at(0);
830         return m.capturedTexts().at(0);
831     }
832     else{
833         return "";
834     }
835 }
836