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:
2026-07-18 09:58:27 +01:00
parent b524e6e33d
commit 989332ef9a
17 changed files with 430 additions and 23 deletions

View File

@@ -26,6 +26,7 @@ struct Options {
std::filesystem::path calendar_path;
std::optional<std::filesystem::path> import_path;
std::optional<std::filesystem::path> export_path;
std::chrono::weekday week_start{std::chrono::Monday};
bool demo{false};
bool print{false};
bool restore_backup{false};
@@ -50,6 +51,7 @@ void print_help(std::ostream& output)
"Usage: nocal [OPTIONS] [CALENDAR.ics]\n\n"
"A theme-native terminal month calendar.\n\n"
" -c, --calendar PATH read events from PATH\n"
" --week-start DAY set first weekday: monday through sunday\n"
" --demo add sample appointments without saving them\n"
" --print print one plain-text frame and exit\n"
" --restore-backup restore PATH.bak over PATH and exit\n"
@@ -68,18 +70,32 @@ void print_help(std::ostream& output)
" t jumps to today, ? toggles help, q quits.\n";
}
std::optional<std::chrono::weekday> parse_week_start(const std::string_view value)
{
if (value == "monday") return std::chrono::Monday;
if (value == "tuesday") return std::chrono::Tuesday;
if (value == "wednesday") return std::chrono::Wednesday;
if (value == "thursday") return std::chrono::Thursday;
if (value == "friday") return std::chrono::Friday;
if (value == "saturday") return std::chrono::Saturday;
if (value == "sunday") return std::chrono::Sunday;
return std::nullopt;
}
Options parse_options(int argc, char** argv)
{
Options options{
.calendar_path = default_calendar_path(),
.import_path = std::nullopt,
.export_path = std::nullopt,
.week_start = std::chrono::Monday,
.demo = false,
.print = false,
.restore_backup = false,
.no_color = false,
};
bool positional_seen = false;
bool week_start_seen = false;
for (int index = 1; index < argc; ++index) {
const std::string_view argument{argv[index]};
if (argument == "-h" || argument == "--help") {
@@ -98,6 +114,22 @@ Options parse_options(int argc, char** argv)
options.restore_backup = true;
} else if (argument == "--no-color") {
options.no_color = true;
} else if (argument == "--week-start") {
if (week_start_seen) {
throw std::invalid_argument("--week-start may only be supplied once");
}
if (++index >= argc) {
throw std::invalid_argument("--week-start requires a day");
}
const std::string_view value{argv[index]};
const auto parsed = parse_week_start(value);
if (!parsed) {
throw std::invalid_argument(
"invalid --week-start day: " + std::string{value} +
" (expected monday through sunday)");
}
options.week_start = *parsed;
week_start_seen = true;
} else if (argument == "--import" || argument == "--export") {
if (++index >= argc) {
throw std::invalid_argument(std::string{argument} + " requires a path");
@@ -182,7 +214,8 @@ std::vector<nocal::Event> demo_events()
return {std::move(planning), std::move(release), std::move(coffee)};
}
int print_frame(std::span<const nocal::Event> events, bool no_color)
int print_frame(std::span<const nocal::Event> events, bool no_color,
const std::chrono::weekday week_start)
{
const auto day = nocal::today();
const nocal::tui::ScreenState state{
@@ -205,6 +238,7 @@ int print_frame(std::span<const nocal::Event> events, bool no_color)
.agenda_start_day = day,
.agenda_items = {},
.agenda_index = 0,
.week_start = week_start,
};
std::cout << nocal::tui::render_month(events, state) << '\n';
return std::cout ? EXIT_SUCCESS : EXIT_FAILURE;
@@ -304,7 +338,7 @@ int main(int argc, char** argv)
const bool non_interactive = ::isatty(STDIN_FILENO) == 0 ||
::isatty(STDOUT_FILENO) == 0;
if (options.print || non_interactive) {
return print_frame(loaded.events, true);
return print_frame(loaded.events, true, options.week_start);
}
if (options.no_color) {
::setenv("NO_COLOR", "1", 1);
@@ -325,7 +359,8 @@ int main(int argc, char** argv)
};
}
}
nocal::tui::TuiApp app{loaded.events, std::move(saver)};
nocal::tui::TuiApp app{
loaded.events, std::move(saver), options.week_start};
return app.run() == nocal::tui::ExitReason::io_error ? EXIT_FAILURE : EXIT_SUCCESS;
} catch (const std::exception& error) {
std::cerr << "nocal: " << error.what() << '\n';