/* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2023, Karol Baraniecki * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #define ANSI_INVERT_OUTPUT "\e[7m" #define ANSI_RESET_OUTPUT "\e[0m" int constexpr month_width = "01 02 03 04 05 06 07"sv.length(); // three months plus padding between them int constexpr year_width = 3 * month_width + 2 * " "sv.length(); int current_year; int current_month; int current_day; static ErrorOr weekday_index(StringView weekday_name) { auto is_same_weekday_name = [&weekday_name](StringView other) { return AK::StringUtils::equals_ignoring_ascii_case(weekday_name, other); }; if (auto it = AK::find_if(AK::long_day_names.begin(), AK::long_day_names.end(), is_same_weekday_name); !it.is_end()) return it.index(); if (auto it = AK::find_if(AK::short_day_names.begin(), AK::short_day_names.end(), is_same_weekday_name); !it.is_end()) return it.index(); if (auto it = AK::find_if(AK::mini_day_names.begin(), AK::mini_day_names.end(), is_same_weekday_name); !it.is_end()) return it.index(); if (auto numeric_weekday = AK::StringUtils::convert_to_int(weekday_name); numeric_weekday.has_value()) return numeric_weekday.value(); return Error::from_string_view("Unknown weekday name"sv); } static ErrorOr default_weekday_start() { auto calendar_config = TRY(Core::ConfigFile::open_for_app("Calendar"sv)); String default_first_day_of_week = TRY(String::from_utf8(calendar_config->read_entry("View"sv, "FirstDayOfWeek"sv, "Sunday"sv))); return TRY(weekday_index(default_first_day_of_week)); } static ErrorOr month_name(int month) { int month_index = month - 1; if (month_index < 0 || month_index >= static_cast(AK::long_month_names.size())) return Error::from_string_view("Month out of range"sv); return AK::long_month_names.at(month_index); } static ErrorOr weekday_names_header(int start_of_week) { // Generates a header in a style of "Su Mo Tu We Th Fr Sa" Vector weekdays; for (size_t i = 0; i < AK::mini_day_names.size(); i++) { size_t day_index = (i + start_of_week) % mini_day_names.size(); TRY(weekdays.try_append(TRY(String::from_utf8(AK::mini_day_names.at(day_index))))); } return TRY(String::join(' ', weekdays)); } enum class Header { MonthAndYear, Month, }; static ErrorOr> month_lines_to_print(Header header_mode, int start_of_week, int month, int year) { Vector lines; // FIXME: Both the month name and month header text should be provided by a locale String header; switch (header_mode) { case Header::Month: header = TRY(String::from_utf8(TRY(month_name(month)))); break; case Header::MonthAndYear: header = TRY(String::formatted("{} - {}", TRY(month_name(month)), year)); break; } TRY(lines.try_append(TRY(String::formatted("{: ^{}s}", header, month_width)))); TRY(lines.try_append(TRY(weekday_names_header(start_of_week)))); auto date_time = Core::DateTime::create(year, month, 1); int first_day_of_week_for_month = date_time.weekday(); int days_in_month = date_time.days_in_month(); first_day_of_week_for_month += 7 - start_of_week; first_day_of_week_for_month %= 7; Vector days_in_row; int day = 1; for (int i = 1; day <= days_in_month; ++i) { if (i - 1 < first_day_of_week_for_month) { TRY(days_in_row.try_append(TRY(String::from_utf8(" "sv)))); } else { if (year == current_year && month == current_month && day == current_day) { TRY(days_in_row.try_append(TRY(String::formatted(ANSI_INVERT_OUTPUT "{:2}" ANSI_RESET_OUTPUT, day)))); } else { TRY(days_in_row.try_append(TRY(String::formatted("{:2}", day)))); } day++; } if (i % 7 == 0) { TRY(lines.try_append(TRY(String::join(' ', days_in_row)))); days_in_row.clear(); } } TRY(lines.try_append(TRY(String::join(' ', days_in_row)))); return lines; } static void print_months_side_by_side(Vector const& left_month, Vector const& center_month, Vector const& right_month) { for (size_t i = 0; i < left_month.size() || i < center_month.size() || i < right_month.size(); i++) { StringView left = i < left_month.size() ? left_month[i] : ""sv; StringView center = i < center_month.size() ? center_month[i] : ""sv; StringView right = i < right_month.size() ? right_month[i] : ""sv; outln("{: <{}} {: <{}} {: <{}}", left, month_width, center, month_width, right, month_width); } } static void go_to_next_month(int& month, int& year) { month += 1; if (month > 12) { year += 1; month = 1; } } static void go_to_previous_month(int& month, int& year) { month -= 1; if (month < 1) { year -= 1; month = 12; } } ErrorOr serenity_main(Main::Arguments arguments) { TRY(Core::System::pledge("stdio rpath cpath")); int month = 0; int year = 0; StringView week_start_day_name {}; bool three_month_mode = false; bool year_mode = false; Core::ArgsParser args_parser; args_parser.set_general_help("Display a nice overview of a month or year, defaulting to the current month."); // FIXME: This should ensure one value gets parsed as just a year args_parser.add_positional_argument(month, "Month", "month", Core::ArgsParser::Required::No); args_parser.add_positional_argument(year, "Year", "year", Core::ArgsParser::Required::No); args_parser.add_option(week_start_day_name, "Day that starts the week", "starting-day", 's', "day"); args_parser.add_option(year_mode, "Show the whole year at once", "year", 'y'); args_parser.add_option(three_month_mode, "Show the previous and next month beside the current one", "three-month-view", '3'); args_parser.parse(arguments); if (three_month_mode && year_mode) { warnln("cal: Cannot specify both --year and --three-month-mode at the same time"); return 1; } time_t now = time(nullptr); auto* tm = localtime(&now); current_year = tm->tm_year + 1900; current_month = tm->tm_mon + 1; current_day = tm->tm_mday; // Hack: workaround one value parsing as a month if (month && !year) { year = month; month = 0; } if (!month && year) year_mode = true; int week_start_day; if (week_start_day_name.is_empty()) week_start_day = TRY(default_weekday_start()); else week_start_day = TRY(weekday_index(week_start_day_name)); if (!year) year = current_year; if (!month) month = current_month; if (year_mode) { outln("{: ^{}}", TRY(String::formatted("Year {}", year)), year_width); for (int month_index = 1; month_index < 12; ++month_index) { outln(); outln(); Vector lines_left = TRY(month_lines_to_print(Header::Month, week_start_day, month_index++, year)); Vector lines_center = TRY(month_lines_to_print(Header::Month, week_start_day, month_index++, year)); Vector lines_right = TRY(month_lines_to_print(Header::Month, week_start_day, month_index, year)); print_months_side_by_side(lines_left, lines_center, lines_right); } } else if (three_month_mode) { int month_on_left = month, year_on_left = year; go_to_previous_month(month_on_left, year_on_left); int month_on_right = month, year_on_right = year; go_to_next_month(month_on_right, year_on_right); Vector lines_previous_month = TRY(month_lines_to_print(Header::MonthAndYear, week_start_day, month_on_left, year_on_left)); Vector lines_current_month = TRY(month_lines_to_print(Header::MonthAndYear, week_start_day, month, year)); Vector lines_next_month = TRY(month_lines_to_print(Header::MonthAndYear, week_start_day, month_on_right, year_on_right)); print_months_side_by_side(lines_previous_month, lines_current_month, lines_next_month); } else { Vector lines = TRY(month_lines_to_print(Header::MonthAndYear, week_start_day, month, year)); for (String const& line : lines) { outln("{}", line); } } return 0; }