feat: initialize Nocal terminal calendar
This commit is contained in:
213
src/main.cpp
Normal file
213
src/main.cpp
Normal file
@@ -0,0 +1,213 @@
|
||||
#include "nocal/domain/date.hpp"
|
||||
#include "nocal/storage/ics_store.hpp"
|
||||
#include "nocal/tui/Screen.hpp"
|
||||
#include "nocal/tui/TuiApp.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view version = "0.1.0";
|
||||
|
||||
struct Options {
|
||||
std::filesystem::path calendar_path;
|
||||
bool demo{false};
|
||||
bool print{false};
|
||||
bool restore_backup{false};
|
||||
bool no_color{false};
|
||||
};
|
||||
|
||||
std::filesystem::path default_calendar_path()
|
||||
{
|
||||
if (const char* data_home = std::getenv("XDG_DATA_HOME");
|
||||
data_home != nullptr && *data_home != '\0') {
|
||||
return std::filesystem::path{data_home} / "nocal" / "calendar.ics";
|
||||
}
|
||||
if (const char* home = std::getenv("HOME"); home != nullptr && *home != '\0') {
|
||||
return std::filesystem::path{home} / ".local" / "share" / "nocal" / "calendar.ics";
|
||||
}
|
||||
return "calendar.ics";
|
||||
}
|
||||
|
||||
void print_help(std::ostream& output)
|
||||
{
|
||||
output <<
|
||||
"Usage: nocal [OPTIONS] [CALENDAR.ics]\n\n"
|
||||
"A theme-native terminal month calendar.\n\n"
|
||||
" -c, --calendar PATH read events from PATH\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"
|
||||
" --no-color disable ANSI styling\n"
|
||||
" -h, --help show this help\n"
|
||||
" -V, --version show the version\n\n"
|
||||
"Keys: arrows/hjkl move days, PageUp/PageDown or p/n change month,\n"
|
||||
" Tab/Shift-Tab focus appointments, Enter reads, Esc returns,\n"
|
||||
" a adds, e edits, d deletes; Ctrl-S saves inside the editor,\n"
|
||||
" u undoes the last change, Ctrl-R redoes it,\n"
|
||||
" t jumps to today, ? toggles help, q quits.\n";
|
||||
}
|
||||
|
||||
Options parse_options(int argc, char** argv)
|
||||
{
|
||||
Options options{.calendar_path = default_calendar_path()};
|
||||
bool positional_seen = false;
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string_view argument{argv[index]};
|
||||
if (argument == "-h" || argument == "--help") {
|
||||
print_help(std::cout);
|
||||
std::exit(EXIT_SUCCESS);
|
||||
}
|
||||
if (argument == "-V" || argument == "--version") {
|
||||
std::cout << "nocal " << version << '\n';
|
||||
std::exit(EXIT_SUCCESS);
|
||||
}
|
||||
if (argument == "--demo") {
|
||||
options.demo = true;
|
||||
} else if (argument == "--print") {
|
||||
options.print = true;
|
||||
} else if (argument == "--restore-backup") {
|
||||
options.restore_backup = true;
|
||||
} else if (argument == "--no-color") {
|
||||
options.no_color = true;
|
||||
} else if (argument == "-c" || argument == "--calendar") {
|
||||
if (++index >= argc) {
|
||||
throw std::invalid_argument(std::string{argument} + " requires a path");
|
||||
}
|
||||
options.calendar_path = argv[index];
|
||||
positional_seen = true;
|
||||
} else if (!argument.empty() && argument.front() == '-') {
|
||||
throw std::invalid_argument("unknown option: " + std::string{argument});
|
||||
} else if (positional_seen) {
|
||||
throw std::invalid_argument("only one calendar path may be supplied");
|
||||
} else {
|
||||
options.calendar_path = argument;
|
||||
positional_seen = true;
|
||||
}
|
||||
}
|
||||
if (options.restore_backup && options.demo) {
|
||||
throw std::invalid_argument("--restore-backup cannot be combined with --demo");
|
||||
}
|
||||
if (options.restore_backup && options.print) {
|
||||
throw std::invalid_argument("--restore-backup cannot be combined with --print");
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
nocal::Date shifted(nocal::Date date, int offset)
|
||||
{
|
||||
return nocal::from_sys_days(nocal::to_sys_days(date) + std::chrono::days{offset});
|
||||
}
|
||||
|
||||
std::vector<nocal::Event> demo_events()
|
||||
{
|
||||
const auto day = nocal::today();
|
||||
nocal::Event planning;
|
||||
planning.uid = "demo-planning@nocal";
|
||||
planning.title = "Nomarchy planning";
|
||||
planning.start = nocal::make_local_time(day, 9, 30);
|
||||
planning.end = nocal::make_local_time(day, 10, 15);
|
||||
planning.location = "Terminal 1";
|
||||
planning.calendar_id = "work";
|
||||
|
||||
nocal::Event release;
|
||||
release.uid = "demo-release@nocal";
|
||||
release.title = "Release window";
|
||||
release.start = nocal::local_day_start(shifted(day, 2));
|
||||
release.end = nocal::local_day_start(shifted(day, 3));
|
||||
release.all_day = true;
|
||||
release.calendar_id = "work";
|
||||
|
||||
nocal::Event coffee;
|
||||
coffee.uid = "demo-coffee@nocal";
|
||||
coffee.title = "Coffee with Luna";
|
||||
coffee.start = nocal::make_local_time(shifted(day, -3), 14, 0);
|
||||
coffee.end = nocal::make_local_time(shifted(day, -3), 14, 45);
|
||||
coffee.location = "Nomarchy café";
|
||||
coffee.calendar_id = "personal";
|
||||
|
||||
return {std::move(planning), std::move(release), std::move(coffee)};
|
||||
}
|
||||
|
||||
int print_frame(std::span<const nocal::Event> events, bool no_color)
|
||||
{
|
||||
const auto day = nocal::today();
|
||||
const nocal::tui::ScreenState state{
|
||||
.visible_month = day.year() / day.month(), .selected_day = day, .today = day,
|
||||
.width = 120, .height = 36, .show_help = false,
|
||||
.ansi = !no_color && ::isatty(STDOUT_FILENO) != 0,
|
||||
.focused_event_id = std::nullopt,
|
||||
.show_event_details = false,
|
||||
.confirm_delete = false,
|
||||
.notification = {},
|
||||
.notification_is_error = false,
|
||||
};
|
||||
std::cout << nocal::tui::render_month(events, state) << '\n';
|
||||
return std::cout ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
try {
|
||||
const auto options = parse_options(argc, argv);
|
||||
auto loaded = nocal::storage::IcsStore::load(options.calendar_path);
|
||||
for (const auto& warning : loaded.warnings) {
|
||||
std::cerr << "nocal: " << warning << '\n';
|
||||
}
|
||||
if (options.restore_backup) {
|
||||
const auto backup = nocal::storage::IcsStore::backup_path(options.calendar_path);
|
||||
nocal::storage::IcsStore::restore_backup(
|
||||
options.calendar_path, loaded.revision);
|
||||
std::cout << "Restored backup " << backup.string() << " to "
|
||||
<< options.calendar_path.string() << '\n';
|
||||
return std::cout ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
if (options.demo) {
|
||||
auto samples = demo_events();
|
||||
loaded.events.insert(loaded.events.end(), samples.begin(), samples.end());
|
||||
}
|
||||
const bool non_interactive = ::isatty(STDIN_FILENO) == 0 ||
|
||||
::isatty(STDOUT_FILENO) == 0;
|
||||
if (options.print || non_interactive) {
|
||||
return print_frame(loaded.events, true);
|
||||
}
|
||||
if (options.no_color) {
|
||||
::setenv("NO_COLOR", "1", 1);
|
||||
}
|
||||
nocal::tui::TuiApp::SaveCallback saver;
|
||||
if (!options.demo) {
|
||||
if (loaded.safe_to_rewrite) {
|
||||
const auto calendar_path = options.calendar_path;
|
||||
saver = [calendar_path, revision = loaded.revision](
|
||||
const std::span<const nocal::Event> events) mutable {
|
||||
revision = nocal::storage::IcsStore::save(
|
||||
calendar_path, events, revision);
|
||||
};
|
||||
} else {
|
||||
saver = [](std::span<const nocal::Event>) {
|
||||
throw std::runtime_error(
|
||||
"calendar is read-only because it contains unsupported iCalendar data");
|
||||
};
|
||||
}
|
||||
}
|
||||
nocal::tui::TuiApp app{loaded.events, std::move(saver)};
|
||||
return app.run() == nocal::tui::ExitReason::io_error ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "nocal: " << error.what() << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user