diff --git a/README b/README new file mode 100644 index 0000000..d84d717 --- /dev/null +++ b/README @@ -0,0 +1,62 @@ +tft - Terminal Family Tree +========================== +tft is an ncurses program that can create and display family trees on the terminal. + +Webpage +------- +https://vene.volconst.com/en/tft + +Dependecies +----------- +ncursesw shared library and headers (libncursesw5-dev on Debian/Ubuntu) + +Installation +------------ +Make sure you satisfy the dependencies and execute: + git clone https://gitlab.com/venelin98/tft.git + cd tft + sudo ./install.sh + +Creating trees +-------------- +Trees are created in plain text files following the tft file format. +Spaces are not important but new lines are considered as separators. + +Adding a member +--------------- +A new line begining with a name is considered a member definition, the name can include spaces. + +After the name the sex can be specifed with a comma followed by either M or F, if not given male is assumed. + +For example: + Adam, M + Declares member Adam that is male. + +Adding member information +------------------------- +In the lines following a member declaration, various info can be added: +s: - Spouses, must be followed by comma separated names of already declared members, "-" can be given instead in which case the last declared person of the opposite sex is used +p: - Parents, must be followed by 1 or 2 names separated by a comma, "-" can be given instead in which case the previous parents are used +b: - Birthday in the format yyyy.mm.dd, day and month can be ommited +d: - Death date + +Duplicate names +--------------- +A number after a name is used to disambiguate when multiple people have identical names e.g.: + "Adam 0" is the first added Adam, "Adam 1" is the second + +Viewing trees +------------- +After creating a tree file you can view it using tft: + tft my_tree.tft + +Once the tree is displayed you can navigate it using: + arrows - move the camera + h, j, k, l - move around the tree + Enter, Space - display the selected person's tree + + - show full names + - - show first names only + w - close the current tree + +УПЪТВАНЕ НА БЪЛГАРСКИ: +https://vene.volconst.com/bg/tft diff --git a/install.sh b/install.sh index fb2d1d0..83e55aa 100755 --- a/install.sh +++ b/install.sh @@ -32,4 +32,4 @@ g++ -o tft $language $optimizations $warnings $other -DNDEBUG -pipe -s *.cpp -ln #-static -lncursesw -ltinfo chmod 775 tft -exec mv tft /usr/local/bin +mv tft /usr/local/bin diff --git a/readme.txt b/readme.txt deleted file mode 100644 index 3ff3078..0000000 --- a/readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -For information visit: -https://vene.volconst.com/en/tft - -Упътване на български: -https://vene.volconst.com/bg/tft diff --git a/src/parser.cpp b/src/parser.cpp index 333908b..2df958d 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -10,7 +10,6 @@ using std::string_view; using std::string; using std::vector; using std::pair; -/* using std::endl; */ namespace /* internal */ { diff --git a/src/person.cpp b/src/person.cpp index ee4d21b..2c37394 100644 --- a/src/person.cpp +++ b/src/person.cpp @@ -60,7 +60,7 @@ bool Person::operator==(const Person &other)const void Person::displayInfo()const { - WINDOW* info_tab = newwin(LINES, COLS, 0, 0); + AutoWin info_tab( newwin(LINES, COLS, 0, 0) ); waddstr(info_tab, name.data()); waddch(info_tab,'\n'); @@ -78,7 +78,6 @@ void Person::displayInfo()const werase(info_tab); wrefresh(info_tab); /* todo: dont update */ - delwin(info_tab); } @@ -118,7 +117,7 @@ U16 Person::draw(const I16 drawY, const I16 drawX, bool selected, bool hasSpouse if(willBeVisible(drawY, drawX, BOX_HEIGHT, width)) { - WINDOW* pad = newpad(BOX_HEIGHT, width); + AutoWin pad( newpad(BOX_HEIGHT, width) ); auto color_pair = COLOR_PAIR( selected ? COLOR_SELECTED : (sex==M ? COLOR_MALE : COLOR_FEMALE) ); @@ -138,7 +137,6 @@ U16 Person::draw(const I16 drawY, const I16 drawX, bool selected, bool hasSpouse /* wattroff(pad, color_pair); */ drawpad(pad, drawY, drawX); - delwin(pad); } return width; } diff --git a/src/tft.cpp b/src/tft.cpp index a266525..d41deac 100644 --- a/src/tft.cpp +++ b/src/tft.cpp @@ -2,49 +2,35 @@ #include "utils.hpp" #include "parser.hpp" #include -/* #include */ #include #include -/* using std::string_view; */ using std::cout; -/* using namespace std::literals; */ int main(int argc, char* argv[]) { if(argc < 2) { cout << "Usage: tft FILES...\n"; - return 0; + return 1; } if(!strcmp(argv[1], "--help")) { cout << "Usage: tft FILES...\n" - << "Display the combined family tree in the FILES\n"; + << "Display the combined family tree from the FILES\n\n" + << VISUALIZATION_HELP_STR; return 0; } Tree tree( parseTftFile(argv[1]) ); - /* bool negative = false; */ for(int i = 2; i < argc; ++i) { tree += parseTftFile(argv[i]); - /* if( !strcmp(argv[i], "-") ) */ - /* { */ - /* negative = true; */ - /* } */ - /* else */ - /* { */ - /* if(negative) */ - /* tree -= parseTftFile(argv[i]); */ - /* else */ - /* tree += parseTftFile(argv[i]); */ - /* } */ } - init(); + init_ncurses(); tree.display(); return 0; diff --git a/src/tree.cpp b/src/tree.cpp index d9b37e2..798a035 100644 --- a/src/tree.cpp +++ b/src/tree.cpp @@ -18,15 +18,15 @@ Tree Tree::operator+ (const Tree& other) const Tree& Tree::operator+=(const Tree& other) { vector newId; //The indexes people from II tree will have in I tree - newId.reserve(other.people.size()); + newId.reserve(other.people_.size()); - U32 initNum = people.size(); /* number of people at the start */ + U32 initNum = people_.size(); /* number of people at the start */ U32 idCounter = initNum; - for (person_id i = 0, j; i < other.people.size(); ++i) //find the new ids for people in II, people present in both trees get the ids from I + for (person_id i = 0, j; i < other.people_.size(); ++i) //find the new ids for people in II, people present in both trees get the ids from I { for (j = 0; j < initNum; ++j) { - if (other.people[i] == people[j]) + if (other.people_[i] == people_[j]) { newId[i] = j; break; @@ -36,31 +36,31 @@ Tree& Tree::operator+=(const Tree& other) newId[i] = idCounter++; } - people.reserve(idCounter); - for (person_id i = 0; i < other.people.size(); ++i) /* add the personal data of II to I */ + people_.reserve(idCounter); + for (person_id i = 0; i < other.people_.size(); ++i) /* add the personal data of II to I */ { if (newId[i] >= initNum) /* isnt already in I */ - people.push_back(other.people[i]); + people_.push_back(other.people_[i]); } - relations.reserve(idCounter); - for (person_id i = 0; i < other.people.size(); ++i) //merge the relative data + relations_.reserve(idCounter); + for (person_id i = 0; i < other.people_.size(); ++i) //merge the relative data { if (newId[i] >= initNum) /* isnt already in I */ { - relations.push_back(other.relations[i]); - for (Relation& rel: relations.back()) + relations_.push_back(other.relations_[i]); + for (Relation& rel: relations_.back()) rel.id = newId[rel.id]; } else //if present in both trees { person_id idIn1 = newId[i]; - relations[idIn1].reserve(relations[idIn1].size() + other.relations[i].size()); + relations_[idIn1].reserve(relations_[idIn1].size() + other.relations_[i].size()); - for (Relation rel: other.relations[i]) + for (Relation rel: other.relations_[i]) { if(newId[rel.id] >= initNum) /* not already in I */ - relations[idIn1].push_back( {newId[rel.id], rel.type} ); + relations_[idIn1].push_back( {newId[rel.id], rel.type} ); } } @@ -71,11 +71,11 @@ Tree& Tree::operator+=(const Tree& other) Tree& Tree::operator-= (const Tree& other) { - for (U32 i = 0; i < people.size(); ++i) + for (U32 i = 0; i < people_.size(); ++i) { - for (const Person& othPers: other.people) + for (const Person& othPers: other.people_) { - if (people[i] == othPers) + if (people_[i] == othPers) { removePerson(i); --i; @@ -89,20 +89,20 @@ Tree& Tree::operator-= (const Tree& other) void Tree::rename(const char* newName) { - treeName = newName; + treeName_ = newName; } person_id Tree::last() { - return people.size() - 1; + return people_.size() - 1; } vector Tree::findId(const string& name) const { vector res; - for (person_id i = 0; i < people.size(); ++i) + for (person_id i = 0; i < people_.size(); ++i) { - if (name == people[i].name) + if (name == people_[i].name) res.push_back(i); } return res; @@ -111,9 +111,9 @@ vector Tree::findId(const string& name) const /* person_id Tree::findId(const string& name, Sex s) const */ /* { */ /* Person sought(name, {}, s); */ -/* for (person_id i = 0; i < people.size(); ++i) */ +/* for (person_id i = 0; i < people_.size(); ++i) */ /* { */ -/* if (sought == people[i]) */ +/* if (sought == people_[i]) */ /* return i; */ /* } */ /* return Nobody; */ @@ -135,9 +135,9 @@ person_id Tree::findRootAncestor(person_id id) const person_id Tree::findOldestAncestor(person_id id, Sex sex) const { - for (const Relation& rel: relations[id]) + for (const Relation& rel: relations_[id]) { - if (rel.type == Child && people[ rel.id ].sex == sex) + if (rel.type == Child && people_[ rel.id ].sex == sex) return findOldestAncestor(rel.id, sex); } return id; @@ -148,18 +148,18 @@ person_id Tree::findCommonAncestor(const unsigned first, const unsigned second) if(first == Nobody || second == Nobody) return Nobody; - vector visited(people.size()); //if a member is visited twice he's a common ancestor + vector visited(people_.size()); //if a member is visited twice he's a common ancestor commonAncestorRec(first, visited); commonAncestorRec(second, visited); person_id oldest = Nobody; U16 oldestBday = SHRT_MAX; - for (person_id i = 0; i < people.size(); ++i) + for (person_id i = 0; i < people_.size(); ++i) { - if (visited[i] == 2 && people[i].birth.year < oldestBday) + if (visited[i] == 2 && people_[i].birth.year < oldestBday) { - /* oldestBday = people[i].birth.year; */ //todo figure it out + /* oldestBday = people_[i].birth.year; */ //todo figure it out /* oldest = i; */ return i; } @@ -170,9 +170,9 @@ person_id Tree::findCommonAncestor(const unsigned first, const unsigned second) person_id Tree::findParent(person_id child, Sex parentSex) const { person_id parent = Nobody; - for(const Relation& rel: relations[child]) + for(const Relation& rel: relations_[child]) { - if (rel.type == Child && people[rel.id].sex == parentSex) + if (rel.type == Child && people_[rel.id].sex == parentSex) parent = rel.id; } return parent; @@ -184,7 +184,7 @@ vector Tree::findChildren(person_id person) const return {}; vector children; - for(const Relation& rel: relations[person]) + for(const Relation& rel: relations_[person]) { if (rel.type == Parent) children.push_back(rel.id); @@ -206,7 +206,7 @@ vector Tree::findChildren(person_id person, person_id exclude) const person_id Tree::findSpouse(person_id person) const { person_id spouse = Nobody; - for(const Relation& rel: relations[person]) + for(const Relation& rel: relations_[person]) { if (rel.type == Spouse) { @@ -220,7 +220,7 @@ person_id Tree::findSpouse(person_id person) const RelType Tree::findRelation(person_id first, person_id second) const { - const auto& curRels = relations[first]; + const auto& curRels = relations_[first]; for(const Relation& rel: curRels) { if (rel.id == second) @@ -231,11 +231,11 @@ RelType Tree::findRelation(person_id first, person_id second) const void Tree::addPerson(const char* name, Sex sex, unsigned father, unsigned mother, EventTime birth, EventTime death) { - people.emplace_back(name, birth, sex, death); + people_.emplace_back(name, birth, sex, death); - relations.emplace_back(); /* relations of the new person */ + relations_.emplace_back(); /* relations of the new person */ - person_id new_person_id = people.size() - 1; + person_id new_person_id = people_.size() - 1; addRelation(father, Parent, new_person_id); addRelation(mother, Parent, new_person_id); @@ -250,7 +250,7 @@ void Tree::addPerson(const char* name, Sex sex, unsigned father, unsigned mother bool Tree::addRelation(const unsigned first, const RelType type, const unsigned second, bool update) { - if (first == Nobody || first >= people.size() || second == Nobody || second >= people.size() || type == None) + if (first == Nobody || first >= people_.size() || second == Nobody || second >= people_.size() || type == None) return false; switch (type) @@ -284,17 +284,17 @@ bool Tree::addRelation(const unsigned first, const RelType type, const unsigned void Tree::addRelOneSide(person_id first, RelType type, person_id second) { unsigned i = 0; - for (; i < relations[first].size(); ++i) //Check if they are already relatives + for (; i < relations_[first].size(); ++i) //Check if they are already relatives { - if (relations[first][i].id == second) + if (relations_[first][i].id == second) { - relations[first][i].type = type; + relations_[first][i].type = type; break; } } - if (i >= relations[first].size()) + if (i >= relations_[first].size()) { - relations[first].push_back({second, type}); + relations_[first].push_back({second, type}); } } @@ -306,32 +306,32 @@ void Tree::addRelOneSide(person_id first, RelType type, person_id second) void Tree::removePerson(const person_id id) { person_id relId; - for (Relation rel: relations[id]) + for (Relation rel: relations_[id]) { relId = rel.id; - for (U32 j = 0; j < relations[relId].size(); ++j) + for (U32 j = 0; j < relations_[relId].size(); ++j) { - if (relations[relId][j].id == id) + if (relations_[relId][j].id == id) { - remove(relations[relId], j); + remove(relations_[relId], j); break; } } } - for (unsigned i = 0; i < relations[people.size() - 2].size(); ++i) //Move the last in his place and update IDs + for (unsigned i = 0; i < relations_[people_.size() - 2].size(); ++i) //Move the last in his place and update IDs { - relId = relations[people.size() - 1][i].id; - for (unsigned j = 0; j < relations[relId].size(); ++j) + relId = relations_[people_.size() - 1][i].id; + for (unsigned j = 0; j < relations_[relId].size(); ++j) { - if (relations[relId][j].id == people.size() - 1) + if (relations_[relId][j].id == people_.size() - 1) { - relations[relId][j].id = id; + relations_[relId][j].id = id; break; } } } - remove(relations, id); - remove(people, id); + remove(relations_, id); + remove(people_, id); } /* void Tree::removePerson(const char* personName, short year, unsigned char month, unsigned char day) */ @@ -341,19 +341,19 @@ void Tree::removePerson(const person_id id) void Tree::removeRelation(const unsigned first, const unsigned second) { - for (unsigned i = 0; i < relations[first].size(); ++i) + for (unsigned i = 0; i < relations_[first].size(); ++i) { - if (relations[first][i].id == second) + if (relations_[first][i].id == second) { - remove(relations[first], i); + remove(relations_[first], i); break; } } - for (unsigned i = 0; i < relations[second].size(); ++i) + for (unsigned i = 0; i < relations_[second].size(); ++i) { - if (relations[second][i].id == first) + if (relations_[second][i].id == first) { - remove(relations[second], i); + remove(relations_[second], i); break; } } @@ -361,12 +361,9 @@ void Tree::removeRelation(const unsigned first, const unsigned second) static void displayHelp() { - WINDOW* help_tab = newwin(LINES, COLS, 0, 0); + AutoWin help_tab( newwin(LINES, COLS, 0, 0) ); - waddstr(help_tab, "Enter - focus the currently selected person\n" /* todo */ - "Arrow keys - move the camera\n" - "H, J, K, L - move the selection\n" - "I - display info about the selected person\n"); + waddstr(help_tab, VISUALIZATION_HELP_STR); mvwaddstr(help_tab, LINES-1, 0, "Press any key to return."); @@ -375,12 +372,11 @@ static void displayHelp() werase(help_tab); wnoutrefresh(help_tab); - delwin(help_tab); } void Tree::display() { - if(people.size() == 0) + if(people_.size() == 0) return; bool to_draw = true; @@ -436,7 +432,7 @@ void Tree::display() selectRight(); break; case DISPLAY_INFO: - people[selected_].displayInfo(); + people_[selected_].displayInfo(); break; default: to_draw = false; /* invalid key, don't redraw */ @@ -492,19 +488,19 @@ void Tree::draw() const void Tree::print() const { - for(const Person& member: people) + for(const Person& member: people_) member.displayInfo(); } void Tree::printMember(const unsigned id)const { - people[id].displayInfo(); + people_[id].displayInfo(); } void Tree::printOldestAncestors(const unsigned id)const { - people[findOldestAncestor(id, M)].displayInfo(); - people[findOldestAncestor(id, F)].displayInfo(); + people_[findOldestAncestor(id, M)].displayInfo(); + people_[findOldestAncestor(id, F)].displayInfo(); } @@ -512,7 +508,7 @@ void Tree::commonAncestorRec(person_id id, vector &visited) const { ++visited[id]; - auto& curRels = relations[id]; + auto& curRels = relations_[id]; for (const Relation& rel: curRels) { if (rel.type == Child) @@ -520,11 +516,12 @@ void Tree::commonAncestorRec(person_id id, vector &visited) const } } +// return the height of the space occupied by the link static U8 linkToFirstBorn(I16 startY, I16 startX, bool moreSiblings) { if (willBeVisible(startY, startX, 3, 1)) { - WINDOW* pad = newpad(3, 1); + AutoWin pad( newpad(3, 1) ); waddch(pad, ACS_VLINE); mvwaddch(pad, 1, 0, @@ -532,7 +529,6 @@ static U8 linkToFirstBorn(I16 startY, I16 startX, bool moreSiblings) mvwaddch(pad, 2, 0, ACS_VLINE); drawpad(pad, startY, startX); - delwin(pad); } return 3; } @@ -543,7 +539,7 @@ static U8 linkToChild(I16 startY, I16 startX, I16 endX, bool moreSiblings) const I16 len = endX - startX; if (willBeVisible(startY, startX, 2, len)) { - WINDOW* pad = newpad(2, len); + AutoWin pad( newpad(2, len) ); for(U16 i = 0; i < len-1; ++i) waddch(pad, ACS_HLINE); @@ -553,7 +549,6 @@ static U8 linkToChild(I16 startY, I16 startX, I16 endX, bool moreSiblings) mvwaddch(pad, 1, len-1, ACS_VLINE); drawpad(pad, startY, startX); - delwin(pad); } return 2; } @@ -561,13 +556,13 @@ static U8 linkToChild(I16 startY, I16 startX, I16 endX, bool moreSiblings) /* I16 Tree::drawLine(person_id id, I16 drawY, I16 drawX) const */ /* { */ -/* people[id].draw(drawY, drawX, true, false, true); */ +/* people_[id].draw(drawY, drawX, true, false, true); */ /* vector children; */ -/* for(U32 i=0; i children; @@ -612,7 +607,7 @@ I16 Tree::drawLineWithWives(const person_id id, I16 drawY, const I16 drawX) cons U16 spouse_w = 0; if (spouse != Nobody) { - U16 spouse_box = people[spouse].draw(drawY, drawX + person_w + spouse_dist_, + U16 spouse_box = people_[spouse].draw(drawY, drawX + person_w + spouse_dist_, spouse == selected_, false, false, false, zoom_==1); spouse_w = spouse_dist_ + spouse_box; } @@ -658,7 +653,7 @@ void Tree::selectLeft() void Tree::selectDown() { - const auto& curRels = relations[selected_]; + const auto& curRels = relations_[selected_]; for(const Relation& rel: curRels) { diff --git a/src/tree.hpp b/src/tree.hpp index eb83e54..5675d30 100644 --- a/src/tree.hpp +++ b/src/tree.hpp @@ -5,6 +5,10 @@ #include "person.hpp" using person_id = U32; +enum: person_id { + Nobody = UINT32_MAX +}; + enum RelType : U8 { None = 0, @@ -24,16 +28,12 @@ struct Relation RelType type; }; -enum: person_id { - Nobody = UINT_MAX -}; - class Tree { public: Tree() = default; template - explicit Tree(Str&& Name) : treeName(std::forward(Name)) {} + explicit Tree(Str&& Name) : treeName_(std::forward(Name)) {} /* Tree(const Tree&) = default; */ /* Tree& operator= (const Tree& other); */ @@ -96,17 +96,17 @@ private: void selectLeftSibSpouse(person_id); void selectRightSib(person_id); - std::string treeName; - std::vector people; //The data for each member - std::vector> relations; //The relatives of each member + std::string treeName_; + std::vector people_; //The data for each member + std::vector> relations_; //The relatives of each member person_id focused_ = 0; /* the tree was draw based on this person */ person_id selected_ = 0; I16 offsetX_ = 0; /* offset of the tree in relation to the screen*/ I16 offsetY_ = 0; U8 zoom_ = 0; - U8 dist_between_ = 1; /* distance between people */ - U8 spouse_dist_ = 0; /* distance between spouses */ + U8 dist_between_ = 1; /* distance between people */ + U8 spouse_dist_ = 0; /* distance between spouses */ //Search, ect. }; diff --git a/src/utils.cpp b/src/utils.cpp index becb9e6..1abf245 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -6,7 +6,21 @@ #include #include -void init() +AutoWin::AutoWin(WINDOW* win) + : w_(win) +{} + +AutoWin::~AutoWin() +{ + delwin(w_); +} + +AutoWin::operator WINDOW*() +{ + return w_; +} + +void init_ncurses() { setlocale(LC_ALL, "en_US.UTF-8"); @@ -66,3 +80,13 @@ void drawpad(WINDOW* pad, I16 drawY, I16 drawX) drawY, drawX, LINES - 1, COLS - 1); } + +I32 min(I32 a, I32 b) +{ + return a <= b ? a : b; +} + +I32 max(I32 a, I32 b) +{ + return a >= b ? a : b; +} diff --git a/src/utils.hpp b/src/utils.hpp index e54bac1..5fe543e 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -1,27 +1,23 @@ #pragma once -#include #include #include "common/basicTypes.h" -/* template */ -/* Vec& operator+=(Vec& lhs, Vec&& rhs) // Combine 2 vectors */ -/* { */ -/* lhs.reserve(lhs.size() + rhs.size()); */ +#define VISUALIZATION_HELP_STR "Visualization defaults (can be changed in config.h):\n" \ +"Enter - focus the currently selected person\n" \ +"Arrow keys - move the camera\n" \ +"h, j, k, l - move the selection\n" \ +"i - display info about the selected person\n" -/* for(auto&& el: rhs) */ -/* lhs.push_back( std::move(el) ); */ -/* return lhs; */ -/* } */ +class AutoWin // ncurses windows that is freed on destruction +{ +public: + explicit AutoWin(WINDOW*); + ~AutoWin(); -/* template */ -/* Vec& operator+=(Vec& lhs, const Vec& rhs) // Combine 2 vectors */ -/* { */ -/* lhs.reserve(lhs.size() + rhs.size()); */ - -/* for(const auto& el: rhs) */ -/* lhs.push_back(el); */ -/* return lhs; */ -/* } */ + operator WINDOW*(); +private: + WINDOW* w_; +}; using fd_t = int; /* file descriptor */ struct MappedFile @@ -30,31 +26,24 @@ struct MappedFile unsigned len; }; -void init(); +void init_ncurses(); MappedFile mapfile(fd_t); fd_t openOrDie(const char* path); -template -void remove(Vec& vec, size_t pos) /* Remove from vector by moving the last element in its place */ -{ - vec[pos] = vec[vec.size() - 1]; - vec.pop_back(); -} - -inline I32 min(I32 a, I32 b) -{ - return a <= b ? a : b; -} - -inline I32 max(I32 a, I32 b) -{ - return a >= b ? a : b; -} - /* wheather a pad with the given coordinates will be visible */ bool willBeVisible(I16 drawY, I16 drawX, U16 height, U16 width); /* like pnoutrefresh but pminrow,col are calculated based on width,height and top left coords, which can be negative (outside the screen) */ void drawpad(WINDOW* pad, I16 drawY, I16 drawX); + +I32 min(I32 a, I32 b); +I32 max(I32 a, I32 b); + +template +void remove(Vec& vec, size_t pos) /* Remove from vector by moving the last element in its place */ +{ + vec[pos] = vec[vec.size() - 1]; + vec.pop_back(); +} diff --git a/todo.txt b/todo.txt index fd2bb34..af7764e 100644 --- a/todo.txt +++ b/todo.txt @@ -1,7 +1,3 @@ ---help - -help - based on user config - findRelative findRelation - extend