#include "todotxt.h" // Todo.txt file format: https://github.com/ginatrapani/todo.txt-cli/wiki/The-Todo.txt-Format #include #include #include #include #include #include #include #include #include #include #include "def.h" todotxt::todotxt() { // This is part of the old implementation where we'd have a constant undoDir that could // be shared by many applications. I leave it here in case we end up with using too much storage // and need manual cleanups. //cleanupUndoDir(); undoDir = new QTemporaryDir(); if(!undoDir->isValid()){ qDebug()<<"Could not create undo dir"<path()< &output){ // Obsolete... remove? Q_UNUSED(filter); for(vector::iterator iter=todo.begin();iter!=todo.end();iter++){ if( (*iter).length()==0 || (*iter).at(0) == 'x') continue; output.push_back( (*iter)); } } bool todotxt::isInactive(QString &text){ QSettings settings; QString t=settings.value(SETTINGS_INACTIVE).toString(); if(t.isEmpty()) return false; QStringList inactives = t.split(";"); for(int i=0;i0){ return true; // Don't show this one since it's in the future } } } if(settings.value(SETTINGS_THRESHOLD_LABELS).toBool()){ auto matches=regex_threshold_project.globalMatch(t); while(matches.hasNext()){ QString project = matches.next().captured(1); if(active_projects.count(project)==1) return true; // There is an active project with this name, so we skip this } matches=regex_threshold_context.globalMatch(t); while(matches.hasNext()){ QString project = matches.next().captured(1); if(active_contexts.count(project)==1) return true; // There is an active project with this name, so we skip this } } return false; } void todotxt::getAll(QString& filter,vector &output){ // Vectors are probably not the best here... Q_UNUSED(filter); set prio; vector open; vector done; vector inactive; QSettings settings; QString t=settings.value(SETTINGS_INACTIVE).toString(); QStringList inactives = t.split(";"); if(!t.contains(";")){ // There is really nothing here but inactives will still have one item. Lets just remove it inactives.clear(); } bool separateinactives = settings.value(SETTINGS_SEPARATE_INACTIVES).toBool(); for(vector::iterator iter=todo.begin();iter!=todo.end();iter++){ QString line = (*iter); // For debugging if(line.isEmpty()) continue; // Begin by checking for inactive, as there are two different ways of sorting those bool inact=false; for(int i=0;i::iterator iter=prio.begin();iter!=prio.end();iter++) output.push_back((*iter)); for(vector::iterator iter=open.begin();iter!=open.end();iter++) output.push_back((*iter)); for(vector::iterator iter=inactive.begin();iter!=inactive.end();iter++) output.push_back((*iter)); for(vector::iterator iter=done.begin();iter!=done.end();iter++) output.push_back((*iter)); } Qt::CheckState todotxt::getState(QString& row){ if(row.length()>1 && row.at(0)=='x' && row.at(1)==' '){ return Qt::Checked; } else { return Qt::Unchecked; } } QString todotxt::getToday(){ QDate d = QDate::currentDate(); return d.toString("yyyy-MM-dd"); } QString todotxt::getRelativeDate(QString shortform,QDate d){ QString extra = ""; // The short form supported for now is +\\dd QRegularExpression reldateregex("\\+(\\d+)([dwmypb])"); QRegularExpressionMatch m = reldateregex.match(shortform); if(m.hasMatch()){ if(m.captured(2).contains('d')){ d= d.addDays(m.captured(1).toInt()); } else if(m.captured(2).contains('w')){ d= d.addDays(m.captured(1).toInt()*7); } else if(m.captured(2).contains('m')){ d= d.addMonths(m.captured(1).toInt()); } else if(m.captured(2).contains('y')) { d= d.addYears(m.captured(1).toInt()); } else if(m.captured(2).contains('p')){ // Ok. This is the procrastination 'feature'. Add a random number of days and also say that this was procrastrinated d = d.addDays(rand()%m.captured(1).toInt()+1); //extra = " +procrastinated"; } else if (m.captured(2).contains('b')){ // Business days. Naive implementation // 1=Monday, 6=Sat, 7=sun int days=0; int addDays = m.captured(1).toInt(); while(daysundoPointer+1){ // yep. there is more in the buffer. // Ok. Here it is obvious that I should have implemented undoBuffer as a vector and probably have the pointer to be a negative index undoPointer++; restoreFiles(undoBuffer[undoBuffer.size()-(1+undoPointer)]); return true; } return false; } bool todotxt::redo() { // Check if we can if(undoPointer>0){ // yep. there is more in the buffer. // Ok. Here it is obvious that I should have implemented undoBuffer as a vector and probably have the pointer to be a negative index undoPointer--; restoreFiles(undoBuffer[undoBuffer.size()-(1+undoPointer)]); return true; } return false; } bool todotxt::undoPossible() { if((int) undoBuffer.size()>undoPointer+1){ return true; } return false; } bool todotxt::redoPossible() { if(undoPointer>0){ return true; } return false; } QString todotxt::getUndoDir() { if(undoDir->isValid()){ return undoDir->path()+"/"; } // The below code is backup for undo directory created in the same folder // in case there was a problem with getting a temp directory (shouldn't happen.. but) QSettings settings; QString uuid = settings.value(SETTINGS_UUID,DEFAULT_UUID).toString(); QString dirbase = settings.value(SETTINGS_DIRECTORY).toString(); QString dir = dirbase+".todour_undo_"+uuid+"/"; // Check that the dir exists QDir directory = QDir(dir); if(!directory.exists()){ directory.mkdir(dir); } return dir; } QString todotxt::getNewUndoNameDirAndPrefix() { // Make a file path that is made to have _todo.txt or _done.txt appended to it // We want the name to be anything that doesn't crash with any other. This can be linear numbering or just an UUID // For now it's UUID but a linear numbering (with a solution to multiple clients running with the same undo-directory) would make // more sense and be easier to follow for end-users in case they are looking through the files. return getUndoDir()+QUuid::createUuid().toString()+"_"; } void todotxt::cleanupUndoDir() { // Go through the directory and remove everything that is older than 14 days old. // Why 14 days? Because. That's why :) // (I simply don't think this is interesting to have configurable.. But simple enough to do in case needed) QDir directory(getUndoDir()); QStringList files = directory.entryList(QDir::Files); QDateTime expirationTime = QDateTime::currentDateTime(); expirationTime = expirationTime.addDays(-14); qDebug()<<"Checking for undo files to cleanup..."< todo; vector lastUndo; if(undoBuffer.empty()){ return true; } QString todofile = getTodoFilePath(); QString undofile = undoBuffer.back()+(TODOFILE); slurp(todofile,todo); slurp(undofile,lastUndo); if(todo.size()!=lastUndo.size()){ qDebug()<<"Sizes differ: "<& content){ QSettings settings; QFile file(filename); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return; QTextStream in(&file); in.setCodec("UTF-8"); while (!in.atEnd()) { QString line = in.readLine(); if(settings.value(SETTINGS_REMOVE_DOUBLETS,DEFAULT_REMOVE_DOUBLETS).toBool()){ // This can be optimized by for example using a set if(std::find(content.begin(),content.end(),line) != content.end()){ // We found this line. So we ignore it continue; } } content.push_back(line); } } void todotxt::write(QString& filename,vector& content){ // As we're about to write a change to the file, we have to consider what is now in the file as valid // 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 undoPointer=0; saveToUndo(); //qDebug()<<"todotxt::write("< deleteddata; slurp(deletedfile,deleteddata); deleteddata.push_back(line); write(deletedfile,deleteddata); } QString tmp; update(line,false,tmp); } void todotxt::archive(){ // Slurp the files QSettings settings; QString todofile = getTodoFilePath(); QString donefile = getDoneFilePath(); vector tododata; vector donedata; slurp(todofile,tododata); slurp(donefile,donedata); for(vector::iterator iter=tododata.begin();iter!=tododata.end();){ if((*iter).length()>0 && (*iter).at(0)=='x'){ donedata.push_back((*iter)); iter=tododata.erase(iter); } else { // No change iter++; } } write(todofile,tododata); write(donefile,donedata); parse(); } void todotxt::refresh(){ parse(); } void todotxt::update(QString &row, bool checked, QString &newrow){ // First slurp the file. QSettings settings; QString todofile = getTodoFilePath(); vector data; slurp(todofile,data); 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 // Preprocessing of the line if(settings.value(SETTINGS_THRESHOLD).toBool()){ QRegularExpression threshold_shorthand("(t:\\+\\d+[dwmypb])"); QRegularExpressionMatch m = threshold_shorthand.match(newrow); if(m.hasMatch()){ newrow = newrow.replace(m.captured(1),"t:"+getRelativeDate(m.captured(1).mid(2))); } } if(settings.value(SETTINGS_DUE).toBool()){ QRegularExpression due_shorthand("(due:\\+\\d+[dwmypb])"); QRegularExpressionMatch m = due_shorthand.match(newrow); if(m.hasMatch()){ newrow = newrow.replace(m.captured(1),"due:"+getRelativeDate(m.captured(1).mid(2))); } } if(row.isEmpty()){ todoline tl; String2Todo(newrow,tl); // Add a date to the line if where doing dates if(settings.value(SETTINGS_DATES).toBool()){ QString today = getToday()+" "; tl.createdDate = today; } // Just add the line data.push_back(Todo2String(tl)); } else { for(vector::iterator iter=data.begin();iter!=data.end();iter++){ QString *r = &(*iter); if(!r->compare(row)){ // Here it is.. Lets modify if we shouldn't remove it alltogether if(newrow.isEmpty()){ // Remove it iter=data.erase(iter); break; } if(checked && !r->startsWith("x ")){ todoline tl; String2Todo(*r,tl); tl.checked=true; QString date; if(settings.value(SETTINGS_DATES).toBool()){ date.append(getToday()+" "); // Add a date if needed } tl.closedDate=date; // Handle recurrence //QRegularExpression rec_normal("(rec:\\d+[dwmyb])"); QRegularExpression rec("(rec:\\+?\\d+[dwmybp])"); // Get the "addition" from rec QRegularExpressionMatch m = rec.match(tl.text); if(m.hasMatch()){ // Figure out what date we should use bool isStrict = true; QString rec_add = m.captured(1).mid(4); // Add a '+' if it's not there due to how getRelativeDate works if(rec_add.at(0)!= '+'){ rec_add.insert(0,'+'); isStrict = false; } // Make a copy. It's time to start altering that one if(!tl.priority.isEmpty()){ additional_item = tl.priority+tl.text; } else { additional_item = tl.text; } // Handle the special case that we're in a rec but there is neither a due nor t if(!regex_threshold_date.match(tl.text).hasMatch() && !regex_due_date.match(tl.text).hasMatch()){ // The rec doesn't have any real point without having a t or a due. // Add one with todays date additional_item.append(" "+settings.value(SETTINGS_DEFAULT_THRESHOLD,DEFAULT_DEFAULT_THRESHOLD).toString()+getToday()); } // Get the t: auto mt = regex_threshold_date.globalMatch(additional_item); while(mt.hasNext()){ QString old_t = mt.next().captured(1); QString newdate = isStrict?getRelativeDate(rec_add, QDate::fromString(old_t,"yyyy-MM-dd")):getRelativeDate(rec_add); additional_item.replace("t:"+old_t,"t:"+newdate); } // Get the due: auto md = regex_due_date.match(additional_item); if(md.hasMatch()){ QString old_due = md.captured(1); QString newdate = isStrict?getRelativeDate(rec_add, QDate::fromString(old_due,"yyyy-MM-dd")):getRelativeDate(rec_add); additional_item.replace("due:"+old_due,"due:"+newdate); } } *r=Todo2String(tl); } else if(!checked && r->startsWith("x ")){ todoline tl; String2Todo(*r,tl); tl.checked=false; tl.closedDate=""; *r=Todo2String(tl); } else { todoline tl; String2Todo(row,tl); todoline newtl; String2Todo(newrow,newtl); tl.priority=newtl.priority; tl.text=newtl.text; tl.createdDate = newtl.createdDate; tl.closedDate = newtl.closedDate; *r = Todo2String(tl); } break; } } } write(todofile,data); if(!additional_item.isEmpty()){ QString empty=""; this->update(empty,false,additional_item); } parse(); } // A todo.txt line looks like this 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+)?(.*)"); void todotxt::String2Todo(QString &line,todoline &t){ QRegularExpressionMatch match = todo_line.match(line); if(match.hasMatch() && match.lastCapturedIndex()==5){ if(match.captured(1).isEmpty()){ t.checked=false; } else { t.checked=true; } t.priority=match.captured(2); if(t.checked){ t.closedDate=match.captured(3); t.createdDate=match.captured(4); } else { t.createdDate=match.captured(3); // No closed date on a line that isn't closed. } t.text = match.captured(5); } else { t.checked=false; t.priority=""; t.closedDate=""; t.createdDate=""; t.text=""; } } QString todotxt::Todo2String(todoline &t){ QString ret; QSettings settings; // Yep, an ugly side effect, but it make sure we're having the right format all the time if(t.checked && t.createdDate.isEmpty()){ t.createdDate = t.closedDate; } if(t.checked){ ret.append("x "); } else { // Priority shall only be written if we are active ret.append(t.priority); } ret.append(t.closedDate); ret.append(t.createdDate); // Here we have to decide how to handle priority tag if we have one if(t.checked && !t.priority.isEmpty()){ prio_on_close how = (prio_on_close) settings.value(SETTINGS_PRIO_ON_CLOSE,DEFAULT_PRIO_ON_CLOSE).toInt(); switch(how){ case prio_on_close::removeit: break; // We do nothing. Just forget it exists case prio_on_close::moveit: ret.append(t.priority+" "); // Put the priority first in the text break; case prio_on_close::tagit: if(t.priority.size()>2){ ret.append("pri:"); ret.append(t.priority.at(1)); ret.append(" "); } break; } } ret.append(t.text); return ret; } // Check when this is due int todotxt::dueIn(QString &text){ int ret=INT_MAX; QSettings settings; if(settings.value(SETTINGS_DUE).toBool()){ QRegularExpressionMatch m=regex_due_date.match(text); if(m.hasMatch()){ QString ds = m.captured(1); QDate d = QDate::fromString(ds,"yyyy-MM-dd"); return (int) QDate::currentDate().daysTo(d); } } return ret; } //QRegularExpression regex_url("[a-zA-Z0-9_]+://[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=\\(\\)]*)"); static QRegularExpression regex_url("[a-zA-Z0-9_]+:\\/\\/([-a-zA-Z0-9@:%_\\+.~#?&\\/=\\(\\)\\{\\}\\\\]*)"); QString todotxt::getURL(QString &line){ QRegularExpressionMatch m=regex_url.match(line); if(m.hasMatch()){ //qDebug()<<"URL: "<