int CmdTimesheet::execute (std::string& output) { int rc = 0; // Scan the pending tasks. handleRecurrence (); std::vector <Task> all = context.tdb2.all_tasks (); context.tdb2.commit (); // What day of the week does the user consider the first? int weekStart = Date::dayOfWeek (context.config.get ("weekstart")); if (weekStart != 0 && weekStart != 1) throw std::string (STRING_DATE_BAD_WEEKSTART); // Determine the date of the first day of the most recent report. Date today; Date start; start -= (((today.dayOfWeek () - weekStart) + 7) % 7) * 86400; // Roll back to midnight. start = Date (start.month (), start.day (), start.year ()); Date end = start + (7 * 86400); // Determine how many reports to run. int quantity = 1; std::vector <std::string> words = context.a3.extract_words (); if (words.size () == 1) quantity = strtol (words[0].c_str (), NULL, 10);; std::stringstream out; for (int week = 0; week < quantity; ++week) { Date endString (end); endString -= 86400; std::string title = start.toString (context.config.get ("dateformat")) + " - " + endString.toString (context.config.get ("dateformat")); Color bold (Color::nocolor, Color::nocolor, false, true, false); out << "\n" << (context.color () ? bold.colorize (title) : title) << "\n"; // Render the completed table. ViewText completed; completed.width (context.getWidth ()); completed.add (Column::factory ("string", " ")); completed.add (Column::factory ("string", STRING_COLUMN_LABEL_PROJECT)); completed.add (Column::factory ("string.right", STRING_COLUMN_LABEL_DUE)); completed.add (Column::factory ("string", STRING_COLUMN_LABEL_DESC)); std::vector <Task>::iterator task; for (task = all.begin (); task != all.end (); ++task) { // If task completed within range. if (task->getStatus () == Task::completed) { Date compDate (task->get_date ("end")); if (compDate >= start && compDate < end) { Color c (task->get ("fg") + " " + task->get ("bg")); if (context.color ()) autoColorize (*task, c); int row = completed.addRow (); std::string format = context.config.get ("dateformat.report"); if (format == "") format = context.config.get ("dateformat"); completed.set (row, 1, task->get ("project"), c); if(task->has ("due")) { Date dt (task->get_date ("due")); completed.set (row, 2, dt.toString (format)); } std::string description = task->get ("description"); int indent = context.config.getInteger ("indent.annotation"); std::map <std::string, std::string> annotations; task->getAnnotations (annotations); std::map <std::string, std::string>::iterator ann; for (ann = annotations.begin (); ann != annotations.end (); ++ann) description += "\n" + std::string (indent, ' ') + Date (ann->first.substr (11)).toString (context.config.get ("dateformat")) + " " + ann->second; completed.set (row, 3, description, c); } } } out << " " << format (STRING_CMD_TIMESHEET_DONE, completed.rows ()) << "\n"; if (completed.rows ()) out << completed.render () << "\n"; // Now render the started table. ViewText started; started.width (context.getWidth ()); started.add (Column::factory ("string", " ")); started.add (Column::factory ("string", STRING_COLUMN_LABEL_PROJECT)); started.add (Column::factory ("string.right", STRING_COLUMN_LABEL_DUE)); started.add (Column::factory ("string", STRING_COLUMN_LABEL_DESC)); for (task = all.begin (); task != all.end (); ++task) { // If task started within range, but not completed withing range. if (task->getStatus () == Task::pending && task->has ("start")) { Date startDate (task->get_date ("start")); if (startDate >= start && startDate < end) { Color c (task->get ("fg") + " " + task->get ("bg")); if (context.color ()) autoColorize (*task, c); int row = started.addRow (); std::string format = context.config.get ("dateformat.report"); if (format == "") format = context.config.get ("dateformat"); started.set (row, 1, task->get ("project"), c); if(task->has ("due")) { Date dt (task->get_date ("due")); started.set (row, 2, dt.toString (format)); } std::string description = task->get ("description"); int indent = context.config.getInteger ("indent.annotation"); std::map <std::string, std::string> annotations; task->getAnnotations (annotations); std::map <std::string, std::string>::iterator ann; for (ann = annotations.begin (); ann != annotations.end (); ++ann) description += "\n" + std::string (indent, ' ') + Date (ann->first.substr (11)).toString (context.config.get ("dateformat")) + " " + ann->second; started.set (row, 3, description, c); } } } out << " " << format (STRING_CMD_TIMESHEET_STARTED, started.rows ()) << "\n"; if (started.rows ()) out << started.render () << "\n\n"; // Prior week. start -= 7 * 86400; end -= 7 * 86400; } output = out.str (); return rc; }
//////////////////////////////////////////////////////////////////////////////// // |<---------- terminal width ---------->| // // +-------+ +-------+ +-------+ // |header | |header | |header | // +--+--+-------+--+-------+--+-------+--+ // |ma|ex|cell |in|cell |in|cell |ex| // +--+--+-------+--+-------+--+-------+--+ // |ma|ex|cell |in|cell |in|cell |ex| // +--+--+-------+--+-------+--+-------+--+ // // margin - indentation for the whole table // extrapadding - left and right padding for the whole table // intrapadding - padding between columns // // // Layout Algorithm: // - Height is irrelevant // - Determine the usable horizontal space for N columns: // // usable = width - ma - (ex * 2) - (in * (N - 1)) // // - Look at every column, for every task, and determine the minimum and // maximum widths. The minimum is the length of the largest indivisible // word, and the maximum is the full length of the value. // - If there is sufficient terminal width to display every task using the // maximum width, then do so. // - If there is insufficient terminal width to display every task using the // minimum width, then there is no layout solution. Error. // - Otherwise there is a need for column wrapping. Calculate the overage, // which is the difference between the sum of the minimum widths and the // usable width. // - Start by using all the minimum column widths, and distribute the overage // among all columns, one character at a time, while the column width is // less than the maximum width, and while there is overage remaining. // // Note: a possible enhancement is to proportionally distribute the overage // according to average data length. // // Note: an enhancement to the 'no solution' problem is to simply force-break // the larger fields. If the widest field is W0, and the second widest // field is W1, then a solution may be achievable by reducing W0 --> W1. // std::string ViewTask::render (std::vector <Task>& data, std::vector <int>& sequence) { context.timer_render.start (); bool const print_empty_columns = context.config.getBoolean ("print.empty.columns"); std::vector <Column*> nonempty_columns; // Determine minimal, ideal column widths. std::vector <int> minimal; std::vector <int> ideal; std::vector <Column*>::iterator i; for (i = _columns.begin (); i != _columns.end (); ++i) { // Headers factor in to width calculations. unsigned int global_min = 0; unsigned int global_ideal = global_min; for (unsigned int s = 0; s < sequence.size (); ++s) { if ((int)s >= _truncate_lines && _truncate_lines != 0) break; if ((int)s >= _truncate_rows && _truncate_rows != 0) break; // Determine minimum and ideal width for this column. unsigned int min; unsigned int ideal; (*i)->measure (data[sequence[s]], min, ideal); if (min > global_min) global_min = min; if (ideal > global_ideal) global_ideal = ideal; } if (print_empty_columns || global_min != 0) { unsigned int label_length = utf8_width ((*i)->label ()); if (label_length > global_min) global_min = label_length; if (label_length > global_ideal) global_ideal = label_length; minimal.push_back (global_min); ideal.push_back (global_ideal); } if (! print_empty_columns && global_min != 0) { nonempty_columns.push_back (*i); } } if (! print_empty_columns) _columns = nonempty_columns; int all_extra = _left_margin + (2 * _extra_padding) + ((_columns.size () - 1) * _intra_padding); // Sum the widths. int sum_minimal = std::accumulate (minimal.begin (), minimal.end (), 0); int sum_ideal = std::accumulate (ideal.begin (), ideal.end (), 0); // Calculate final column widths. int overage = _width - sum_minimal - all_extra; context.debug (format ("ViewTask::render min={1} ideal={2} overage={3}", sum_minimal + all_extra, sum_ideal + all_extra, overage)); std::vector <int> widths; // Ideal case. Everything fits. if (_width == 0 || sum_ideal + all_extra <= _width) { widths = ideal; } // Not enough for minimum. else if (overage < 0) { context.error (format (STRING_VIEW_TOO_SMALL, sum_minimal + all_extra, _width)); widths = minimal; } // Perfect minimal width. else if (overage == 0) { widths = minimal; } // Extra space to share. else if (overage > 0) { widths = minimal; // Spread 'overage' among columns where width[i] < ideal[i] bool needed = true; while (overage && needed) { needed = false; for (unsigned int i = 0; i < _columns.size () && overage; ++i) { if (widths[i] < ideal[i]) { ++widths[i]; --overage; needed = true; } } } } // Compose column headers. unsigned int max_lines = 0; std::vector <std::vector <std::string> > headers; for (unsigned int c = 0; c < _columns.size (); ++c) { headers.push_back (std::vector <std::string> ()); _columns[c]->renderHeader (headers[c], widths[c], _header); if (headers[c].size () > max_lines) max_lines = headers[c].size (); } // Output string. std::string out; _lines = 0; // Render column headers. std::string left_margin = std::string (_left_margin, ' '); std::string extra = std::string (_extra_padding, ' '); std::string intra = std::string (_intra_padding, ' '); std::string extra_odd = context.color () ? _extra_odd.colorize (extra) : extra; std::string extra_even = context.color () ? _extra_even.colorize (extra) : extra; std::string intra_odd = context.color () ? _intra_odd.colorize (intra) : intra; std::string intra_even = context.color () ? _intra_even.colorize (intra) : intra; for (unsigned int i = 0; i < max_lines; ++i) { out += left_margin + extra; for (unsigned int c = 0; c < _columns.size (); ++c) { if (c) out += intra; if (headers[c].size () < max_lines - i) out += _header.colorize (std::string (widths[c], ' ')); else out += headers[c][i]; } out += extra; // Trim right. out.erase (out.find_last_not_of (" ") + 1); out += "\n"; // Stop if the line limit is exceeded. if (++_lines >= _truncate_lines && _truncate_lines != 0) { context.timer_render.stop (); return out; } } // Compose, render columns, in sequence. _rows = 0; std::vector <std::vector <std::string> > cells; std::vector <int>::iterator s; for (unsigned int s = 0; s < sequence.size (); ++s) { max_lines = 0; // Apply color rules to task. Color rule_color; autoColorize (data[sequence[s]], rule_color); // Alternate rows based on |s % 2| bool odd = (s % 2) ? true : false; Color row_color; if (context.color ()) { row_color = odd ? _odd : _even; row_color.blend (rule_color); } for (unsigned int c = 0; c < _columns.size (); ++c) { cells.push_back (std::vector <std::string> ()); _columns[c]->render (cells[c], data[sequence[s]], widths[c], row_color); if (cells[c].size () > max_lines) max_lines = cells[c].size (); } for (unsigned int i = 0; i < max_lines; ++i) { out += left_margin + (odd ? extra_odd : extra_even); for (unsigned int c = 0; c < _columns.size (); ++c) { if (c) { if (row_color.nontrivial ()) out += row_color.colorize (intra); else out += (odd ? intra_odd : intra_even); } if (i < cells[c].size ()) out += cells[c][i]; else out += row_color.colorize (std::string (widths[c], ' ')); } out += (odd ? extra_odd : extra_even); // Trim right. out.erase (out.find_last_not_of (" ") + 1); out += "\n"; // Stop if the line limit is exceeded. if (++_lines >= _truncate_lines && _truncate_lines != 0) { context.timer_render.stop (); return out; } } cells.clear (); // Stop if the row limit is exceeded. if (++_rows >= _truncate_rows && _truncate_rows != 0) { context.timer_render.stop (); return out; } } context.timer_render.stop (); return out; }
int CmdInfo::execute (std::string& output) { int rc = 0; // Apply filter. std::vector <Task> filtered; filter (filtered); if (! filtered.size ()) { context.footnote (STRING_FEEDBACK_NO_MATCH); rc = 1; } // Get the undo data. std::vector <std::string> undo; if (context.config.getBoolean ("journal.info")) undo = context.tdb2.undo.get_lines (); // Determine the output date format, which uses a hierarchy of definitions. // rc.dateformat.info // rc.dateformat std::string dateformat = context.config.get ("dateformat.info"); if (dateformat == "") dateformat = context.config.get ("dateformat"); std::string dateformatanno = context.config.get ("dateformat.annotation"); if (dateformatanno == "") dateformatanno = dateformat; // Render each task. std::stringstream out; std::vector <Task>::iterator task; for (task = filtered.begin (); task != filtered.end (); ++task) { ViewText view; view.width (context.getWidth ()); view.add (Column::factory ("string", STRING_COLUMN_LABEL_NAME)); view.add (Column::factory ("string", STRING_COLUMN_LABEL_VALUE)); // If an alternating row color is specified, notify the table. if (context.color ()) { Color alternate (context.config.get ("color.alternate")); view.colorOdd (alternate); view.intraColorOdd (alternate); } Date now; // id int row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_ID); view.set (row, 1, (task->id ? format (task->id) : "-")); std::string status = ucFirst (Task::statusToText (task->getStatus ())); // description Color c; autoColorize (*task, c); std::string description = task->get ("description"); int indent = context.config.getInteger ("indent.annotation"); std::map <std::string, std::string> annotations; task->getAnnotations (annotations); std::map <std::string, std::string>::iterator ann; for (ann = annotations.begin (); ann != annotations.end (); ++ann) description += "\n" + std::string (indent, ' ') + Date (ann->first.substr (11)).toString (dateformatanno) + " " + ann->second; row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_DESC); view.set (row, 1, description, c); // status row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_STATUS); view.set (row, 1, status); // project if (task->has ("project")) { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_PROJECT); view.set (row, 1, task->get ("project")); } // priority if (task->has ("priority")) { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_PRIORITY); view.set (row, 1, task->get ("priority")); } // dependencies: blocked { std::vector <Task> blocked; dependencyGetBlocking (*task, blocked); if (blocked.size ()) { std::stringstream message; std::vector <Task>::const_iterator it; for (it = blocked.begin (); it != blocked.end (); ++it) message << it->id << " " << it->get ("description") << "\n"; row = view.addRow (); view.set (row, 0, STRING_CMD_INFO_BLOCKED); view.set (row, 1, message.str ()); } } // dependencies: blocking { std::vector <Task> blocking; dependencyGetBlocked (*task, blocking); if (blocking.size ()) { std::stringstream message; std::vector <Task>::const_iterator it; for (it = blocking.begin (); it != blocking.end (); ++it) message << it->id << " " << it->get ("description") << "\n"; row = view.addRow (); view.set (row, 0, STRING_CMD_INFO_BLOCKING); view.set (row, 1, message.str ()); } } // recur if (task->has ("recur")) { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_RECUR_L); view.set (row, 1, task->get ("recur")); } // until if (task->has ("until")) { row = view.addRow (); view.set (row, 0, STRING_CMD_INFO_UNTIL); view.set (row, 1, Date (task->get_date ("until")).toString (dateformat)); } // mask if (task->getStatus () == Task::recurring) { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_MASK); view.set (row, 1, task->get ("mask")); } if (task->has ("parent")) { // parent row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_PARENT); view.set (row, 1, task->get ("parent")); // imask row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_MASK_IDX); view.set (row, 1, task->get ("imask")); } // due (colored) if (task->has ("due")) { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_DUE); view.set (row, 1, Date (task->get_date ("due")).toString (dateformat)); } // wait if (task->has ("wait")) { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_WAITING); view.set (row, 1, Date (task->get_date ("wait")).toString (dateformat)); } // scheduled if (task->has ("scheduled")) { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_SCHED); view.set (row, 1, Date (task->get_date ("scheduled")).toString (dateformat)); } // start if (task->has ("start")) { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_START); view.set (row, 1, Date (task->get_date ("start")).toString (dateformat)); } // end if (task->has ("end")) { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_END); view.set (row, 1, Date (task->get_date ("end")).toString (dateformat)); } // tags ... std::vector <std::string> tags; task->getTags (tags); if (tags.size ()) { std::string allTags; join (allTags, " ", tags); row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_TAGS); view.set (row, 1, allTags); } // uuid row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_UUID); std::string uuid = task->get ("uuid"); view.set (row, 1, uuid); // entry row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_ENTERED); Date dt (task->get_date ("entry")); std::string entry = dt.toString (dateformat); std::string age; std::string created = task->get ("entry"); if (created.length ()) { Date dt (strtol (created.c_str (), NULL, 10)); age = Duration (now - dt).format (); } view.set (row, 1, entry + " (" + age + ")"); // fg TODO deprecated 2.0 std::string color = task->get ("fg"); if (color != "") { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_FG); view.set (row, 1, color); } // bg TODO deprecated 2.0 color = task->get ("bg"); if (color != "") { row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_BG); view.set (row, 1, color); } // Task::urgency row = view.addRow (); view.set (row, 0, STRING_COLUMN_LABEL_URGENCY); view.set (row, 1, trimLeft (format (task->urgency (), 4, 4))); // Show any UDAs std::vector <std::string> all = task->all (); std::vector <std::string>::iterator att; std::string type; for (att = all.begin (); att != all.end (); ++att) { type = context.config.get ("uda." + *att + ".type"); if (type != "") { Column* col = context.columns[*att]; if (col) { std::string value = task->get (*att); if (value != "") { row = view.addRow (); view.set (row, 0, col->label ()); if (type == "date") value = Date (value).toString (dateformat); else if (type == "duration") value = Duration (value).formatCompact (); view.set (row, 1, value); } } } } // Show any orphaned UDAs, which are identified by not being represented in // the context.columns map. for (att = all.begin (); att != all.end (); ++att) if (att->substr (0, 11) != "annotation_" && context.columns.find (*att) == context.columns.end ()) { row = view.addRow (); view.set (row, 0, "[" + *att); view.set (row, 1, task->get (*att) + "]"); } // Create a second table, containing undo log change details. ViewText journal; // If an alternating row color is specified, notify the table. if (context.color ()) { Color alternate (context.config.get ("color.alternate")); journal.colorOdd (alternate); journal.intraColorOdd (alternate); } journal.width (context.getWidth ()); journal.add (Column::factory ("string", STRING_COLUMN_LABEL_DATE)); journal.add (Column::factory ("string", STRING_CMD_INFO_MODIFICATION)); if (context.config.getBoolean ("journal.info") && undo.size () > 3) { // Scan the undo data for entries matching this task. std::string when; std::string previous; std::string current; unsigned int i = 0; long total_time = 0; while (i < undo.size ()) { when = undo[i++]; previous = ""; if (undo[i].substr (0, 3) == "old") previous = undo[i++]; current = undo[i++]; i++; // Separator if (current.find ("uuid:\"" + uuid) != std::string::npos) { if (previous != "") { int row = journal.addRow (); Date timestamp (strtol (when.substr (5).c_str (), NULL, 10)); journal.set (row, 0, timestamp.toString (dateformat)); Task before (previous.substr (4)); Task after (current.substr (4)); journal.set (row, 1, taskInfoDifferences (before, after, dateformat)); // calculate the total active time if (before.get ("start") == "" && after.get ("start") != "") { // task started total_time -= timestamp.toEpoch (); } else if (((before.get ("start") != "" && after.get ("start") == "") || (before.get ("status") != "completed" && after.get ("status") == "completed")) && total_time < 0) { // task stopped or done total_time += timestamp.toEpoch (); } } } } // add now() if task is still active if (total_time < 0) total_time += Date ().toEpoch (); // print total active time if (total_time > 0) { row = journal.addRow (); journal.set (row, 0, STRING_CMD_INFO_TOTAL_ACTIVE); journal.set (row, 1, Duration (total_time).formatPrecise (), (context.color () ? Color ("bold") : Color ())); } } out << optionalBlankLine () << view.render () << "\n"; if (journal.rows () > 0) out << journal.render () << "\n"; } output = out.str (); return rc; }