feat: screen-reader-friendly linear view (L key)
Plain-text appointment list with numbered indicators, no box-drawing characters, and explicit labels. Reuses agenda data and navigation. Binds to uppercase L to preserve vim-style hjkl grid navigation.
This commit is contained in:
@@ -475,6 +475,112 @@ void test_agenda_input_and_search_precedence()
|
||||
"g toggles the agenda view");
|
||||
}
|
||||
|
||||
void test_linear_view_frame()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto state = nocal::tui::ScreenState{};
|
||||
state.visible_month = year{2026} / July;
|
||||
state.selected_day = year{2026} / July / day{17};
|
||||
state.today = state.selected_day;
|
||||
state.width = 80;
|
||||
state.height = 12;
|
||||
state.ansi = false;
|
||||
state.show_linear_view = true;
|
||||
state.agenda_start_day = year{2026} / July / day{1};
|
||||
state.agenda_items = {
|
||||
{.focus_id = "first", .title = "Holiday", .day = year{2026} / July / day{7},
|
||||
.time_of_day = std::nullopt, .all_day = true,
|
||||
.location = {}, .calendar_id = "home", .recurring = false},
|
||||
{.focus_id = "second", .title = "Release", .day = year{2026} / July / day{12},
|
||||
.time_of_day = hours{14} + minutes{30}, .all_day = false,
|
||||
.location = "Studio", .calendar_id = "work", .recurring = true},
|
||||
{.focus_id = "third", .title = "Retrospective", .day = year{2026} / July / day{13},
|
||||
.time_of_day = hours{16}, .all_day = false,
|
||||
.location = "Room 2", .calendar_id = "work", .recurring = false},
|
||||
};
|
||||
state.agenda_index = 0;
|
||||
|
||||
const auto frame = nocal::tui::render_month(
|
||||
std::span<const nocal::tui::CalendarItem>{}, state);
|
||||
|
||||
check(frame.find("LINEAR VIEW") != std::string::npos,
|
||||
"linear view header is rendered");
|
||||
check(frame.find("July 2026") != std::string::npos,
|
||||
"linear view shows the month in the header");
|
||||
check(frame.find("(3 events)") != std::string::npos,
|
||||
"linear view header counts items");
|
||||
check(frame.find("[*>]") != std::string::npos,
|
||||
"linear view marks focused item with [*>] in non-ANSI mode");
|
||||
check(frame.find("[2]") != std::string::npos &&
|
||||
frame.find("[3]") != std::string::npos,
|
||||
"linear view uses numbered indicators for non-focused items");
|
||||
check(frame.find("2026-07-07") != std::string::npos &&
|
||||
frame.find("all day") != std::string::npos &&
|
||||
frame.find("Holiday") != std::string::npos,
|
||||
"linear view renders all-day event with correct format");
|
||||
check(frame.find("2026-07-12 14:30 local") != std::string::npos &&
|
||||
frame.find("Release") != std::string::npos &&
|
||||
frame.find("(recurring)") != std::string::npos &&
|
||||
frame.find(" - Studio") != std::string::npos,
|
||||
"linear view renders timed recurring event with location");
|
||||
check(frame.find("2026-07-13 16:00 local Retrospective - Room 2") != std::string::npos,
|
||||
"linear view renders all fields for an item");
|
||||
check(frame.find("navigate") != std::string::npos &&
|
||||
frame.find("Enter") != std::string::npos &&
|
||||
frame.find("month") != std::string::npos &&
|
||||
frame.find("close") != std::string::npos,
|
||||
"linear view footer shows navigation hints");
|
||||
check(std::count(frame.begin(), frame.end(), '\n') == 11,
|
||||
"linear view fills the requested height");
|
||||
|
||||
// Screen-reader-friendly: no box-drawing chars
|
||||
check(frame.find("\xe2\x94\x8c") == std::string::npos &&
|
||||
frame.find("\xe2\x94\x82") == std::string::npos &&
|
||||
frame.find("\xe2\x94\x94") == std::string::npos &&
|
||||
frame.find("\xe2\x94\x9c") == std::string::npos &&
|
||||
frame.find("\xe2\x94\xa4") == std::string::npos &&
|
||||
frame.find("\xe2\x94\xac") == std::string::npos &&
|
||||
frame.find("\xe2\x94\xb4") == std::string::npos &&
|
||||
frame.find("\xe2\x94\xbc") == std::string::npos,
|
||||
"linear view has no box-drawing characters");
|
||||
// No bullets or chevrons used in month/agenda views
|
||||
check(frame.find("\xe2\x80\xa2") == std::string::npos,
|
||||
"linear view has no bullet character");
|
||||
check(frame.find("\xe2\x80\xba") == std::string::npos,
|
||||
"linear view has no single chevron character");
|
||||
}
|
||||
|
||||
void test_linear_view_empty_frame()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
auto state = nocal::tui::ScreenState{};
|
||||
state.width = 72;
|
||||
state.height = 9;
|
||||
state.ansi = false;
|
||||
state.show_linear_view = true;
|
||||
state.agenda_start_day = year{2026} / July / day{1};
|
||||
|
||||
const auto frame = nocal::tui::render_month(
|
||||
std::span<const nocal::tui::CalendarItem>{}, state);
|
||||
check(frame.find("LINEAR VIEW") != std::string::npos,
|
||||
"empty linear view header is rendered");
|
||||
check(frame.find("(0 events)") != std::string::npos ||
|
||||
frame.find("(0 event)") != std::string::npos,
|
||||
"empty linear view shows zero count");
|
||||
check(frame.find("No appointments.") != std::string::npos,
|
||||
"empty linear view explains no appointments");
|
||||
check(std::count(frame.begin(), frame.end(), '\n') == 8,
|
||||
"empty linear view fills the requested height");
|
||||
check(frame.find("\xe2\x94\x8c") == std::string::npos &&
|
||||
frame.find("\xe2\x94\x82") == std::string::npos &&
|
||||
frame.find("\xe2\x94\x94") == std::string::npos,
|
||||
"empty linear view has no box-drawing characters");
|
||||
check(frame.find("\xe2\x80\xa2") == std::string::npos,
|
||||
"empty linear view has no bullet character");
|
||||
check(frame.find("\xe2\x80\xba") == std::string::npos,
|
||||
"empty linear view has no chevron character");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void test_help_frame()
|
||||
@@ -566,6 +672,8 @@ int main()
|
||||
test_agenda_empty_frame();
|
||||
test_agenda_selection_and_scrolling();
|
||||
test_agenda_input_and_search_precedence();
|
||||
test_linear_view_frame();
|
||||
test_linear_view_empty_frame();
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " TUI test(s) failed\n";
|
||||
return EXIT_FAILURE;
|
||||
|
||||
Reference in New Issue
Block a user