feat: make month week start configurable
Add a Monday-default --week-start option for every weekday and carry it through domain layout, full and compact rendering, event query windows, print mode, and the interactive TUI. Preserve byte-compatible Monday output and cover rotated headers, spill dates, invalid input, sanitizer behavior, and live terminal restoration.
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -22,6 +24,43 @@ void check(bool condition, const char* message)
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view frame_line(const std::string& frame, std::size_t requested)
|
||||
{
|
||||
std::size_t start = 0;
|
||||
for (std::size_t line = 0; line < requested; ++line) {
|
||||
const auto newline = frame.find('\n', start);
|
||||
if (newline == std::string::npos) return {};
|
||||
start = newline + 1;
|
||||
}
|
||||
const auto newline = frame.find('\n', start);
|
||||
return std::string_view{frame}.substr(
|
||||
start, newline == std::string::npos ? frame.size() - start : newline - start);
|
||||
}
|
||||
|
||||
bool ordered(std::string_view line, const std::vector<std::string_view>& labels)
|
||||
{
|
||||
std::size_t position = 0;
|
||||
for (const auto label : labels) {
|
||||
position = line.find(label, position);
|
||||
if (position == std::string_view::npos) return false;
|
||||
position += label.size();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
nocal::Event all_day_event(std::string uid, std::string title,
|
||||
nocal::Date start, nocal::Date exclusive_end)
|
||||
{
|
||||
nocal::Event event;
|
||||
event.uid = std::move(uid);
|
||||
event.title = std::move(title);
|
||||
event.start = nocal::make_local_time(start);
|
||||
event.end = nocal::make_local_time(exclusive_end);
|
||||
event.all_day = true;
|
||||
event.time_basis = nocal::TimeBasis::floating;
|
||||
return event;
|
||||
}
|
||||
|
||||
void test_full_month_render()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
@@ -49,6 +88,7 @@ void test_full_month_render()
|
||||
.agenda_start_day = year{1970} / January / day{1},
|
||||
.agenda_items = {},
|
||||
.agenda_index = 0,
|
||||
.week_start = Monday,
|
||||
};
|
||||
const std::vector<nocal::tui::CalendarItem> items{
|
||||
{.id = "1", .title = "Design review", .day = year{2026} / July / day{17},
|
||||
@@ -105,6 +145,7 @@ void test_compact_and_input()
|
||||
.agenda_start_day = year{1970} / January / day{1},
|
||||
.agenda_items = {},
|
||||
.agenda_index = 0,
|
||||
.week_start = Monday,
|
||||
};
|
||||
const auto frame = nocal::tui::render_month(std::span<const nocal::tui::CalendarItem>{}, state);
|
||||
check(frame.find("July 2026") != std::string::npos, "compact mode keeps month context");
|
||||
@@ -115,6 +156,109 @@ void test_compact_and_input()
|
||||
"c opens the calendar picker");
|
||||
}
|
||||
|
||||
void test_sunday_start_full_render_and_domain_window()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto state = nocal::tui::ScreenState{};
|
||||
state.visible_month = year{2026} / July;
|
||||
state.selected_day = year{2026} / July / day{1};
|
||||
state.today = year{2026} / July / day{17};
|
||||
state.width = 140;
|
||||
state.height = 34;
|
||||
state.ansi = false;
|
||||
state.week_start = Sunday;
|
||||
|
||||
const std::vector<nocal::Event> events{
|
||||
all_day_event("leading", "Leading Sunday", year{2026} / June / day{28},
|
||||
year{2026} / June / day{29}),
|
||||
all_day_event("trailing", "Trailing Saturday", year{2026} / August / day{8},
|
||||
year{2026} / August / day{9}),
|
||||
all_day_event("outside", "Outside Saturday", year{2026} / June / day{27},
|
||||
year{2026} / June / day{28}),
|
||||
};
|
||||
|
||||
const auto frame = nocal::tui::render_month(events, state);
|
||||
const auto header = frame_line(frame, 1);
|
||||
check(ordered(header, {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday",
|
||||
"Friday", "Saturday"}),
|
||||
"Sunday start rotates the full weekday header");
|
||||
check(frame_line(frame, 3).starts_with("│ 28"),
|
||||
"Sunday start places the June 28 spill day in the first cell");
|
||||
check(frame.find("Leading Sunday") != std::string::npos &&
|
||||
frame.find("Trailing") != std::string::npos,
|
||||
"domain adapter includes events at both ends of the configured 42-day grid");
|
||||
check(frame.find("Outside Saturday") == std::string::npos,
|
||||
"domain adapter excludes an event before the configured grid");
|
||||
check(frame.find("\x1b[") == std::string::npos,
|
||||
"Sunday-start full rendering supports ANSI-off output");
|
||||
check(std::count(frame.begin(), frame.end(), '\n') == 33,
|
||||
"Sunday-start full rendering preserves exact requested height");
|
||||
}
|
||||
|
||||
void test_wednesday_start_compact_render()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto state = nocal::tui::ScreenState{};
|
||||
state.visible_month = year{2026} / July;
|
||||
state.selected_day = year{2026} / July / day{1};
|
||||
state.today = year{2026} / July / day{17};
|
||||
state.width = 30;
|
||||
state.height = 12;
|
||||
state.ansi = false;
|
||||
state.week_start = Wednesday;
|
||||
const std::vector<nocal::tui::CalendarItem> items{
|
||||
{.id = "trailing", .title = "Last spill", .day = year{2026} / August / day{11},
|
||||
.time_of_day = std::nullopt, .all_day = true,
|
||||
.end_time_of_day = std::nullopt, .start_day = {}, .end_day = {},
|
||||
.location = {}, .description = {}, .detail_title = {},
|
||||
.detail_all_day = true, .detail_start_time_of_day = std::nullopt,
|
||||
.segment = nocal::tui::ItemSegment::single, .recurring = false,
|
||||
.recurrence_summary = {}, .source_time_zone = {}},
|
||||
};
|
||||
|
||||
const auto frame = nocal::tui::render_month(items, state);
|
||||
check(ordered(frame_line(frame, 1), {"We", "Th", "Fr", "Sa", "Su", "Mo", "Tu"}),
|
||||
"Wednesday start rotates the compact weekday header");
|
||||
check(frame_line(frame, 2).starts_with(" 1 "),
|
||||
"Wednesday start places July 1 in the first compact cell");
|
||||
check(frame_line(frame, 7).find("11•") != std::string_view::npos,
|
||||
"compact rendering marks an event in the final spill cell");
|
||||
check(frame.find("\x1b[") == std::string::npos,
|
||||
"Wednesday-start compact rendering supports ANSI-off output");
|
||||
check(std::count(frame.begin(), frame.end(), '\n') == 11,
|
||||
"Wednesday-start compact rendering preserves exact requested height");
|
||||
}
|
||||
|
||||
void test_monday_default_and_invalid_week_start_fallback()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto default_state = nocal::tui::ScreenState{};
|
||||
default_state.visible_month = year{2026} / July;
|
||||
default_state.selected_day = year{2026} / July / day{17};
|
||||
default_state.today = default_state.selected_day;
|
||||
default_state.width = 72;
|
||||
default_state.height = 20;
|
||||
default_state.ansi = false;
|
||||
|
||||
auto explicit_monday = default_state;
|
||||
explicit_monday.week_start = Monday;
|
||||
const auto default_frame = nocal::tui::render_month(
|
||||
std::span<const nocal::tui::CalendarItem>{}, default_state);
|
||||
const auto explicit_frame = nocal::tui::render_month(
|
||||
std::span<const nocal::tui::CalendarItem>{}, explicit_monday);
|
||||
check(default_frame == explicit_frame,
|
||||
"default week start remains byte-compatible with explicit Monday");
|
||||
check(ordered(frame_line(default_frame, 1), {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}),
|
||||
"default full header remains Monday-first");
|
||||
|
||||
auto invalid = default_state;
|
||||
invalid.week_start = weekday{8};
|
||||
const auto invalid_frame = nocal::tui::render_month(
|
||||
std::span<const nocal::tui::CalendarItem>{}, invalid);
|
||||
check(invalid_frame == default_frame,
|
||||
"invalid week-start state safely falls back to Monday");
|
||||
}
|
||||
|
||||
void test_calendar_picker_frame()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
@@ -293,6 +437,9 @@ int main()
|
||||
{
|
||||
test_full_month_render();
|
||||
test_compact_and_input();
|
||||
test_sunday_start_full_render_and_domain_window();
|
||||
test_wednesday_start_compact_render();
|
||||
test_monday_default_and_invalid_week_start_fallback();
|
||||
test_navigation();
|
||||
test_search_frames();
|
||||
test_calendar_picker_frame();
|
||||
|
||||
Reference in New Issue
Block a user