diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5e5e53e7..d8ecddd5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,7 @@ set (timew_SRCS AtomicFile.cpp AtomicFile.h Journal.cpp Journal.h Range.cpp Range.h Rules.cpp Rules.h + SummaryTable.cpp SummaryTable.h TagDescription.cpp TagDescription.h TagInfo.cpp TagInfo.h TagInfoDatabase.cpp TagInfoDatabase.h diff --git a/src/SummaryTable.cpp b/src/SummaryTable.cpp new file mode 100644 index 00000000..ab63fd2e --- /dev/null +++ b/src/SummaryTable.cpp @@ -0,0 +1,276 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023, Gothenburg Bit Factory. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// https://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder SummaryTable::builder () +{ + return {}; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder& SummaryTable::Builder::withWeekFormat (const std::string& format) +{ + _week_fmt = format; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder& SummaryTable::Builder::withDateFormat (const std::string& format) +{ + _date_fmt = format; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder& SummaryTable::Builder::withTimeFormat (const std::string& format) +{ + _time_fmt = format; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder& SummaryTable::Builder::withAnnotations (const bool show) +{ + _show_annotations = show; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder& SummaryTable::Builder::withIds (bool show, Color color) +{ + _show_ids = show; + _color_id = color; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder & SummaryTable::Builder::withTags (bool show, std::map & colors) +{ + _show_tags = show; + _color_tags = std::move (colors); + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder& SummaryTable::Builder::withWeekdays (const bool show) +{ + _show_weekdays = show; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder& SummaryTable::Builder::withWeeks (const bool show) +{ + _show_weeks = show; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder& SummaryTable::Builder::withRange (const Range& range) +{ + _range = range; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +SummaryTable::Builder & SummaryTable::Builder::withIntervals (const std::vector& tracked) +{ + _tracked = tracked; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +Table SummaryTable::Builder::build () +{ + const auto dates_col_offset = _show_weeks ? 1 : 0; + const auto weekdays_col_offset = dates_col_offset; + const auto ids_col_offset = weekdays_col_offset + (_show_weekdays ? 1: 0); + const auto tags_col_offset = ids_col_offset + (_show_ids ? 1 : 0); + const auto annotation_col_offset = tags_col_offset + (_show_tags ? 1 : 0); + const auto start_col_offset = annotation_col_offset + (_show_annotations ? 1 : 0); + + const auto weeks_col_index = 0; + const auto dates_col_index = 0 + dates_col_offset; + const auto weekdays_col_index = 1 + weekdays_col_offset; + const auto ids_col_index = 1 + ids_col_offset; + const auto tags_col_index = 1 + tags_col_offset; + const auto annotation_col_index = 1 + annotation_col_offset; + const auto start_col_index = 1 + start_col_offset; + const auto end_col_index = 2 + start_col_offset; + const auto duration_col_index = 3 + start_col_offset; + const auto total_col_index = 4 + start_col_offset; + + Table table; + table.width (1024); + table.colorHeader (Color ("underline")); + + if (_show_weeks) + { + table.add ("Wk"); + } + + table.add ("Date"); + + if (_show_weekdays) + { + table.add ("Day"); + } + + if (_show_ids) + { + table.add ("ID"); + } + + if (_show_tags) + { + table.add ("Tags"); + } + + if (_show_annotations) + { + table.add ("Annotation"); + } + + table.add ("Start", false); + table.add ("End", false); + table.add ("Time", false); + table.add ("Total", false); + + // Each day is rendered separately. + time_t grand_total = 0; + Datetime previous; + + auto days_start = _range.is_started () ? _range.start : _tracked.front ().start; + auto days_end = _range.is_ended () ? _range.end : _tracked.back ().end; + + const auto now = Datetime (); + + if (days_end == 0) + { + days_end = now; + } + + for (Datetime day = days_start.startOfDay (); day < days_end; ++day) + { + auto day_range = getFullDay (day); + time_t daily_total = 0; + + int row = -1; + for (auto& track : subset (day_range, _tracked)) + { + // Make sure the track only represents one day. + if ((track.is_open () && day > now)) + { + continue; + } + + row = table.addRow (); + + if (day != previous) + { + if (_show_weeks) + { + table.set (row, weeks_col_index, format (_week_fmt, day.week ())); + } + + table.set (row, dates_col_index, day.toString (_date_fmt)); + + if (_show_weekdays) + { + table.set (row, weekdays_col_index, Datetime::dayNameShort (day.dayOfWeek ())); + } + + previous = day; + } + + // Intersect track with day. + auto today = day_range.intersect (track); + + if (track.is_open () && track.start > now) + { + today.end = track.start; + } + else if (track.is_open () && day <= now && today.end > now) + { + today.end = now; + } + + if (_show_ids) + { + table.set (row, ids_col_index, format ("@{1}", track.id), _color_id); + } + + if (_show_tags) + { + auto tags_string = join (", ", track.tags ()); + table.set (row, tags_col_index, tags_string, summaryIntervalColor (_color_tags, track.tags ())); + } + + if (_show_annotations) + { + auto annotation = track.getAnnotation (); + + if (utf8_length (annotation) > 15) + { + annotation = utf8_substr (annotation, 0, 12) + "..."; + } + + table.set (row, annotation_col_index, annotation); + } + + const auto total = today.total (); + + table.set (row, start_col_index, today.start.toString (_time_fmt)); + table.set (row, end_col_index, (track.is_open () ? "-" : today.end.toString (_time_fmt))); + table.set (row, duration_col_index, Duration (total).formatHours ()); + + daily_total += total; + } + + if (row != -1) + { + table.set (row, total_col_index, Duration (daily_total).formatHours ()); + } + + grand_total += daily_total; + } + + // Add the total. + table.set (table.addRow (), total_col_index, " ", Color ("underline")); + table.set (table.addRow (), total_col_index, Duration (grand_total).formatHours ()); + + return table; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/SummaryTable.h b/src/SummaryTable.h new file mode 100644 index 00000000..21b6a7bd --- /dev/null +++ b/src/SummaryTable.h @@ -0,0 +1,77 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023, Gothenburg Bit Factory. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// https://www.opensource.org/licenses/mit-license.php +// +////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDED_SUMMARYTABLE +#define INCLUDED_SUMMARYTABLE + +#include +#include +#include +#include +#include + +class SummaryTable +{ + class Builder + { + public: + Builder& withWeekFormat (const std::string &); + Builder& withDateFormat (const std::string &); + Builder& withTimeFormat (const std::string &); + + Builder& withAnnotations (bool); + Builder& withIds (bool, Color); + Builder& withTags (bool, std::map &); + Builder& withWeeks (bool); + Builder& withWeekdays (bool); + + Builder& withRange (const Range &); + Builder& withIntervals (const std::vector &); + + Table build (); + + private: + std::string _week_fmt; + std::string _date_fmt; + std::string _time_fmt; + + bool _show_annotations; + bool _show_ids; + bool _show_tags; + bool _show_weekdays; + bool _show_weeks; + + Range _range; + std::vector _tracked; + Color _color_id; + std::map _color_tags; + }; + +public: + static Builder builder (); +}; + +#endif // INCLUDED_SUMMARYTABLE diff --git a/src/commands/CmdSummary.cpp b/src/commands/CmdSummary.cpp index 1e24ea44..7d97485f 100644 --- a/src/commands/CmdSummary.cpp +++ b/src/commands/CmdSummary.cpp @@ -24,10 +24,10 @@ // //////////////////////////////////////////////////////////////////////////////// -#include #include #include #include +#include #include #include #include @@ -92,10 +92,7 @@ int CmdSummary ( // Map tags to colors. Color colorID (rules.getBoolean ("color") ? rules.get ("theme.colors.ids") : ""); - - const auto week_fmt = "W{1}"; - const auto date_fmt = "Y-M-D"; - const auto time_fmt = "h:N:S"; + auto tagColorMap = createTagColorMap (rules, tracked); const auto show_weeks = rules.getBoolean ("reports.summary.weeks", true); const auto show_weekdays = rules.getBoolean ("reports.summary.weekdays", true); @@ -104,162 +101,18 @@ int CmdSummary ( const auto show_annotations = cli.getComplementaryHint ("annotations", rules.getBoolean ("reports.summary.annotations")); const auto show_holidays = cli.getComplementaryHint ("holidays", rules.getBoolean ("reports.summary.holidays")); - const auto dates_col_offset = show_weeks ? 1 : 0; - const auto weekdays_col_offset = dates_col_offset; - const auto ids_col_offset = weekdays_col_offset + (show_weekdays ? 1: 0); - const auto tags_col_offset = ids_col_offset + (show_ids ? 1 : 0); - const auto annotation_col_offset = tags_col_offset + (show_tags ? 1 : 0); - const auto start_col_offset = annotation_col_offset + (show_annotations ? 1 : 0); - - const auto weeks_col_index = 0; - const auto dates_col_index = 0 + dates_col_offset; - const auto weekdays_col_index = 1 + weekdays_col_offset; - const auto ids_col_index = 1 + ids_col_offset; - const auto tags_col_index = 1 + tags_col_offset; - const auto annotation_col_index = 1 + annotation_col_offset; - const auto start_col_index = 1 + start_col_offset; - const auto end_col_index = 2 + start_col_offset; - const auto duration_col_index = 3 + start_col_offset; - const auto total_col_index = 4 + start_col_offset; - - Table table; - table.width (1024); - table.colorHeader (Color ("underline")); - - if (show_weeks) - { - table.add ("Wk"); - } - - table.add ("Date"); - - if (show_weekdays) - { - table.add ("Day"); - } - - if (show_ids) - { - table.add ("ID"); - } - - if (show_tags) - { - table.add ("Tags"); - } - - if (show_annotations) - { - table.add ("Annotation"); - } - - table.add ("Start", false); - table.add ("End", false); - table.add ("Time", false); - table.add ("Total", false); - - // Each day is rendered separately. - time_t grand_total = 0; - Datetime previous; - - auto days_start = range.is_started() ? range.start : tracked.front ().start; - auto days_end = range.is_ended() ? range.end : tracked.back ().end; - - const auto now = Datetime (); - - if (days_end == 0) - { - days_end = now; - } - - for (Datetime day = days_start.startOfDay (); day < days_end; ++day) - { - auto day_range = getFullDay (day); - time_t daily_total = 0; - - int row = -1; - for (auto& track : subset (day_range, tracked)) - { - // Make sure the track only represents one day. - if ((track.is_open () && day > now)) - { - continue; - } - - row = table.addRow (); - - if (day != previous) - { - if (show_weeks) - { - table.set (row, weeks_col_index, format (week_fmt, day.week ())); - } - - table.set (row, dates_col_index, day.toString (date_fmt)); - - if (show_weekdays) - { - table.set (row, weekdays_col_index, Datetime::dayNameShort (day.dayOfWeek ())); - } - - previous = day; - } - - // Intersect track with day. - auto today = day_range.intersect (track); - - if (track.is_open() && track.start > now) - { - today.end = track.start; - } - else if (track.is_open () && day <= now && today.end > now) - { - today.end = now; - } - - if (show_ids) - { - table.set (row, ids_col_index, format ("@{1}", track.id), colorID); - } - - if (show_tags) - { - std::string tags_string = join (", ", track.tags ()); - table.set (row, tags_col_index, tags_string, summaryIntervalColor (rules, track.tags ())); - } - - if (show_annotations) - { - auto annotation = track.getAnnotation (); - - if (utf8_length (annotation) > 15) - { - annotation = utf8_substr (annotation, 0, 12) + "..."; - } - - table.set (row, annotation_col_index, annotation); - } - - const auto total = today.total (); - - table.set (row, start_col_index, today.start.toString (time_fmt)); - table.set (row, end_col_index, (track.is_open () ? "-" : today.end.toString (time_fmt))); - table.set (row, duration_col_index, Duration (total).formatHours ()); - - daily_total += total; - } - - if (row != -1) - { - table.set (row, total_col_index, Duration (daily_total).formatHours ()); - } - - grand_total += daily_total; - } - - // Add the total. - table.set (table.addRow (), total_col_index, " ", Color ("underline")); - table.set (table.addRow (), total_col_index, Duration (grand_total).formatHours ()); + auto table = SummaryTable::builder () + .withWeekFormat ("W{1}") + .withDateFormat ("Y-M-D") + .withTimeFormat ("h:N:S") + .withWeeks (show_weeks) + .withWeekdays (show_weekdays) + .withIds (show_ids, colorID) + .withTags (show_tags, tagColorMap) + .withAnnotations (show_annotations) + .withRange (range) + .withIntervals (tracked) + .build (); std::cout << '\n' << table.render () @@ -270,11 +123,11 @@ int CmdSummary ( } //////////////////////////////////////////////////////////////////////////////// -std::string renderHolidays (const std::map &holidays) +std::string renderHolidays (const std::map& holidays) { std::stringstream out; - for (auto &entry : holidays) + for (auto& entry : holidays) { out << entry.first.toString ("Y-M-D") << " " diff --git a/src/helper.cpp b/src/helper.cpp index c334c4fd..f7daf6b9 100644 --- a/src/helper.cpp +++ b/src/helper.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -50,6 +51,21 @@ Color summaryIntervalColor ( return c; } +//////////////////////////////////////////////////////////////////////////////// +Color summaryIntervalColor ( + std::map & tagColors, + const std::set & tags) +{ + Color c; + + for (const auto& tag : tags) + { + c.blend (tagColors[tag]); + } + + return c; +} + //////////////////////////////////////////////////////////////////////////////// // Select a color to represent the interval on a chart. Color chartIntervalColor ( @@ -410,6 +426,30 @@ std::map createTagColorMap ( return mapping; } +//////////////////////////////////////////////////////////////////////////////// +std::map createTagColorMap (const Rules& rules, const std::vector & intervals) +{ + std::set tags; + + for (const auto& interval : intervals) + { + tags.insert (interval.tags ().begin (), interval.tags ().end ()); + } + + std::map mapping; + + for (const auto& tag : tags) + { + std::string key = "tags." + tag + ".color"; + if (rules.has (key)) + { + mapping[tag] = Color (rules.get (key)); + } + } + + return mapping; +} + //////////////////////////////////////////////////////////////////////////////// int quantizeToNMinutes (const int minutes, const int N) { diff --git a/src/timew.h b/src/timew.h index bb5ddaba..36b9a6f1 100644 --- a/src/timew.h +++ b/src/timew.h @@ -69,6 +69,7 @@ int dispatchCommand (const CLI&, Database&, Journal&, Rules&, const Extensions&) // helper.cpp Color summaryIntervalColor (const Rules&, const std::set &); +Color summaryIntervalColor (std::map &, const std::set &); Color chartIntervalColor (const std::set &, const std::map &); Color tagColor (const Rules&, const std::string&); std::string intervalSummarize (const Rules&, const Interval&); @@ -76,6 +77,7 @@ bool expandIntervalHint (const std::string&, Range&); std::string jsonFromIntervals (const std::vector &); Palette createPalette (const Rules&); std::map createTagColorMap (const Rules&, Palette&, const std::vector &); +std::map createTagColorMap (const Rules& rules, const std::vector & intervals); int quantizeToNMinutes (int, int); bool findHint (const CLI&, const std::string&);