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