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:
@@ -87,6 +87,7 @@ struct ScreenState {
|
|||||||
bool show_calendar_picker{false};
|
bool show_calendar_picker{false};
|
||||||
std::size_t calendar_index{0};
|
std::size_t calendar_index{0};
|
||||||
bool show_agenda{false};
|
bool show_agenda{false};
|
||||||
|
bool show_linear_view{false};
|
||||||
std::chrono::year_month_day agenda_start_day{
|
std::chrono::year_month_day agenda_start_day{
|
||||||
std::chrono::year{1970}, std::chrono::month{1}, std::chrono::day{1}};
|
std::chrono::year{1970}, std::chrono::month{1}, std::chrono::day{1}};
|
||||||
std::vector<AgendaItem> agenda_items;
|
std::vector<AgendaItem> agenda_items;
|
||||||
@@ -138,6 +139,7 @@ enum class Action {
|
|||||||
search,
|
search,
|
||||||
agenda,
|
agenda,
|
||||||
calendars,
|
calendars,
|
||||||
|
linear_view,
|
||||||
sync,
|
sync,
|
||||||
quit,
|
quit,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -121,9 +121,12 @@ private:
|
|||||||
void toggle_selected_calendar();
|
void toggle_selected_calendar();
|
||||||
void begin_agenda();
|
void begin_agenda();
|
||||||
void close_agenda();
|
void close_agenda();
|
||||||
|
void begin_linear_view();
|
||||||
|
void close_linear_view();
|
||||||
void refresh_agenda();
|
void refresh_agenda();
|
||||||
void shift_agenda(int direction);
|
void shift_agenda(int direction);
|
||||||
void choose_agenda_item();
|
void choose_agenda_item();
|
||||||
|
void choose_linear_item();
|
||||||
[[nodiscard]] HistorySnapshot capture_history_snapshot() const;
|
[[nodiscard]] HistorySnapshot capture_history_snapshot() const;
|
||||||
void restore_history_snapshot(HistorySnapshot snapshot);
|
void restore_history_snapshot(HistorySnapshot snapshot);
|
||||||
void push_history(std::vector<HistoryEntry>& history, HistoryEntry entry);
|
void push_history(std::vector<HistoryEntry>& history, HistoryEntry entry);
|
||||||
|
|||||||
@@ -667,6 +667,97 @@ std::string agenda_frame(const ScreenState& state) {
|
|||||||
return output.str();
|
return output.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string linear_frame(const ScreenState& state) {
|
||||||
|
const int width = std::max(1, state.width);
|
||||||
|
const int height = std::max(1, state.height);
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
lines.reserve(static_cast<std::size_t>(height));
|
||||||
|
|
||||||
|
const auto month_index = static_cast<unsigned>(state.visible_month.month()) - 1;
|
||||||
|
const auto month_name = std::string{month_names[month_index]};
|
||||||
|
const auto year_str = std::to_string(static_cast<int>(state.visible_month.year()));
|
||||||
|
const auto count = state.agenda_items.size();
|
||||||
|
const auto header = "LINEAR VIEW \u2014 " + month_name + " " + year_str +
|
||||||
|
" (" + std::to_string(count) + " event" + (count == 1 ? "" : "s") + ")";
|
||||||
|
lines.push_back(styled(centred(header, width), "1", state.ansi));
|
||||||
|
|
||||||
|
std::vector<std::size_t> ordered(state.agenda_items.size());
|
||||||
|
for (std::size_t index = 0; index < ordered.size(); ++index) ordered[index] = index;
|
||||||
|
std::stable_sort(ordered.begin(), ordered.end(), [&](const auto left_index,
|
||||||
|
const auto right_index) {
|
||||||
|
const auto& left = state.agenda_items[left_index];
|
||||||
|
const auto& right = state.agenda_items[right_index];
|
||||||
|
if (left.day != right.day) return sys_days{left.day} < sys_days{right.day};
|
||||||
|
if (left.all_day != right.all_day) return left.all_day;
|
||||||
|
return left.time_of_day.value_or(minutes{-1}) <
|
||||||
|
right.time_of_day.value_or(minutes{-1});
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto footer_rows = 1;
|
||||||
|
const auto row_capacity = static_cast<std::size_t>(
|
||||||
|
std::max(0, height - 2 - footer_rows));
|
||||||
|
const auto selected = std::find(ordered.begin(), ordered.end(), state.agenda_index);
|
||||||
|
const auto selected_position = selected == ordered.end()
|
||||||
|
? std::size_t{0}
|
||||||
|
: static_cast<std::size_t>(selected - ordered.begin());
|
||||||
|
std::size_t first = 0;
|
||||||
|
if (row_capacity > 0 && selected != ordered.end() && selected_position >= row_capacity) {
|
||||||
|
first = selected_position - row_capacity + 1;
|
||||||
|
}
|
||||||
|
const auto last = std::min(ordered.size(), first + row_capacity);
|
||||||
|
for (auto position = first; position < last; ++position) {
|
||||||
|
const auto index = ordered[position];
|
||||||
|
const auto& item = state.agenda_items[index];
|
||||||
|
const bool focused = index == state.agenda_index;
|
||||||
|
const auto number = position - first + 1;
|
||||||
|
|
||||||
|
auto when = item.all_day || !item.time_of_day
|
||||||
|
? std::string{"all day"}
|
||||||
|
: clock_time(*item.time_of_day) + " local";
|
||||||
|
auto title = item.title.empty() ? std::string{"(untitled)"}
|
||||||
|
: printable_text(item.title);
|
||||||
|
|
||||||
|
std::string prefix;
|
||||||
|
if (focused && !state.ansi) {
|
||||||
|
prefix = "[*>]";
|
||||||
|
} else {
|
||||||
|
prefix = "[" + std::to_string(number) + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto label = prefix + " " + iso_date(item.day) + " " + when + " " + title;
|
||||||
|
if (item.recurring) label += " (recurring)";
|
||||||
|
if (!item.location.empty()) label += " - " + printable_text(item.location);
|
||||||
|
|
||||||
|
lines.push_back(styled(fit_text(label, width), focused ? "1" : "", state.ansi));
|
||||||
|
}
|
||||||
|
if (state.agenda_items.empty() && row_capacity > 0) {
|
||||||
|
lines.push_back(styled(centred("No appointments.", width), "2", state.ansi));
|
||||||
|
}
|
||||||
|
while (static_cast<int>(lines.size()) < height - footer_rows) {
|
||||||
|
lines.emplace_back(static_cast<std::size_t>(width), ' ');
|
||||||
|
}
|
||||||
|
if (footer_rows > 0) {
|
||||||
|
const std::array linear_hints{
|
||||||
|
KeyHint{"\xe2\x86\x91\xe2\x86\x93", "navigate"},
|
||||||
|
KeyHint{"Enter", "month"},
|
||||||
|
KeyHint{"n/p", "window"},
|
||||||
|
KeyHint{"L/Esc", "close"},
|
||||||
|
};
|
||||||
|
lines.push_back(render_hints(linear_hints, width, state.ansi));
|
||||||
|
}
|
||||||
|
while (static_cast<int>(lines.size()) < height) {
|
||||||
|
lines.emplace_back(static_cast<std::size_t>(width), ' ');
|
||||||
|
}
|
||||||
|
if (static_cast<int>(lines.size()) > height) lines.resize(static_cast<std::size_t>(height));
|
||||||
|
|
||||||
|
std::ostringstream output;
|
||||||
|
for (int line = 0; line < height; ++line) {
|
||||||
|
if (line != 0) output << '\n';
|
||||||
|
output << lines[static_cast<std::size_t>(line)];
|
||||||
|
}
|
||||||
|
return output.str();
|
||||||
|
}
|
||||||
|
|
||||||
std::string calendar_picker_frame(const std::span<const Calendar> calendars,
|
std::string calendar_picker_frame(const std::span<const Calendar> calendars,
|
||||||
const ScreenState& state) {
|
const ScreenState& state) {
|
||||||
const int width = std::max(1, state.width);
|
const int width = std::max(1, state.width);
|
||||||
@@ -752,6 +843,7 @@ std::string help_frame(const ScreenState& state) {
|
|||||||
{"Ctrl-R", "Redo last change"}}},
|
{"Ctrl-R", "Redo last change"}}},
|
||||||
HelpGroup{"VIEWS",
|
HelpGroup{"VIEWS",
|
||||||
{{"g", "Agenda (42 days)"},
|
{{"g", "Agenda (42 days)"},
|
||||||
|
{"L", "Linear view (accessible)"},
|
||||||
{"c", "Calendar visibility"},
|
{"c", "Calendar visibility"},
|
||||||
{"/", "Search appointments"}}},
|
{"/", "Search appointments"}}},
|
||||||
HelpGroup{"GENERAL",
|
HelpGroup{"GENERAL",
|
||||||
@@ -954,6 +1046,7 @@ Action decode_key(const std::string_view sequence) noexcept {
|
|||||||
if (sequence == "\x12") return Action::redo;
|
if (sequence == "\x12") return Action::redo;
|
||||||
if (sequence == "/") return Action::search;
|
if (sequence == "/") return Action::search;
|
||||||
if (sequence == "g") return Action::agenda;
|
if (sequence == "g") return Action::agenda;
|
||||||
|
if (sequence == "L") return Action::linear_view;
|
||||||
if (sequence == "c") return Action::calendars;
|
if (sequence == "c") return Action::calendars;
|
||||||
if (sequence == "q" || sequence == "Q" || sequence == "\x03") return Action::quit;
|
if (sequence == "q" || sequence == "Q" || sequence == "\x03") return Action::quit;
|
||||||
if (sequence == "\x13") return Action::sync;
|
if (sequence == "\x13") return Action::sync;
|
||||||
@@ -961,6 +1054,7 @@ Action decode_key(const std::string_view sequence) noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string render_month(std::span<const CalendarItem> items, const ScreenState& state) {
|
std::string render_month(std::span<const CalendarItem> items, const ScreenState& state) {
|
||||||
|
if (state.show_linear_view) return linear_frame(state);
|
||||||
if (state.search_prompt || state.show_search_results) return search_frame(state);
|
if (state.search_prompt || state.show_search_results) return search_frame(state);
|
||||||
if (state.show_agenda) return agenda_frame(state);
|
if (state.show_agenda) return agenda_frame(state);
|
||||||
const auto focused = focused_item(items, state);
|
const auto focused = focused_item(items, state);
|
||||||
|
|||||||
@@ -275,6 +275,36 @@ void TuiApp::close_agenda() {
|
|||||||
state_.show_agenda = false;
|
state_.show_agenda = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TuiApp::begin_linear_view() {
|
||||||
|
state_.show_linear_view = true;
|
||||||
|
state_.agenda_start_day = state_.selected_day;
|
||||||
|
state_.agenda_index = 0;
|
||||||
|
state_.agenda_items.clear();
|
||||||
|
state_.show_event_details = false;
|
||||||
|
state_.show_help = false;
|
||||||
|
state_.confirm_delete = false;
|
||||||
|
state_.search_prompt = false;
|
||||||
|
state_.show_search_results = false;
|
||||||
|
state_.show_calendar_picker = false;
|
||||||
|
state_.show_agenda = false;
|
||||||
|
refresh_agenda();
|
||||||
|
// Place cursor on the first item on or after the selected day.
|
||||||
|
if (!state_.agenda_items.empty()) {
|
||||||
|
const auto target = sys_days{state_.selected_day};
|
||||||
|
for (std::size_t index = 0; index < state_.agenda_items.size(); ++index) {
|
||||||
|
if (sys_days{state_.agenda_items[index].day} >= target) {
|
||||||
|
state_.agenda_index = index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state_.agenda_index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TuiApp::close_linear_view() {
|
||||||
|
state_.show_linear_view = false;
|
||||||
|
}
|
||||||
|
|
||||||
void TuiApp::refresh_agenda() {
|
void TuiApp::refresh_agenda() {
|
||||||
if (!state_.agenda_start_day.ok()) state_.agenda_start_day = state_.selected_day;
|
if (!state_.agenda_start_day.ok()) state_.agenda_start_day = state_.selected_day;
|
||||||
std::optional<std::string> selected_identity;
|
std::optional<std::string> selected_identity;
|
||||||
@@ -361,6 +391,16 @@ void TuiApp::choose_agenda_item() {
|
|||||||
close_agenda();
|
close_agenda();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TuiApp::choose_linear_item() {
|
||||||
|
if (state_.agenda_index >= state_.agenda_items.size()) return;
|
||||||
|
const auto& item = state_.agenda_items[state_.agenda_index];
|
||||||
|
state_.selected_day = item.day;
|
||||||
|
state_.visible_month = item.day.year() / item.day.month();
|
||||||
|
state_.focused_event_id = item.focus_id;
|
||||||
|
state_.show_event_details = false;
|
||||||
|
close_linear_view();
|
||||||
|
}
|
||||||
|
|
||||||
void TuiApp::set_notification(std::string message, const bool error) {
|
void TuiApp::set_notification(std::string message, const bool error) {
|
||||||
state_.notification = std::move(message);
|
state_.notification = std::move(message);
|
||||||
state_.notification_is_error = error;
|
state_.notification_is_error = error;
|
||||||
@@ -808,6 +848,7 @@ void TuiApp::dispatch(const Action action) {
|
|||||||
case Action::redo:
|
case Action::redo:
|
||||||
case Action::agenda:
|
case Action::agenda:
|
||||||
case Action::calendars:
|
case Action::calendars:
|
||||||
|
case Action::linear_view:
|
||||||
case Action::sync:
|
case Action::sync:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -850,6 +891,61 @@ void TuiApp::dispatch(const Action action) {
|
|||||||
case Action::redo:
|
case Action::redo:
|
||||||
case Action::search:
|
case Action::search:
|
||||||
case Action::agenda:
|
case Action::agenda:
|
||||||
|
case Action::linear_view:
|
||||||
|
case Action::sync:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state_.show_linear_view) {
|
||||||
|
const auto count = state_.agenda_items.size();
|
||||||
|
switch (action) {
|
||||||
|
case Action::up:
|
||||||
|
case Action::left:
|
||||||
|
case Action::previous_event:
|
||||||
|
if (count > 0 && state_.agenda_index > 0) --state_.agenda_index;
|
||||||
|
return;
|
||||||
|
case Action::down:
|
||||||
|
case Action::right:
|
||||||
|
case Action::next_event:
|
||||||
|
if (count > 0 && state_.agenda_index + 1 < count) ++state_.agenda_index;
|
||||||
|
return;
|
||||||
|
case Action::previous_month:
|
||||||
|
shift_agenda(-1);
|
||||||
|
return;
|
||||||
|
case Action::next_month:
|
||||||
|
shift_agenda(1);
|
||||||
|
return;
|
||||||
|
case Action::today:
|
||||||
|
state_.today = current_day();
|
||||||
|
state_.agenda_start_day = state_.today;
|
||||||
|
state_.agenda_items.clear();
|
||||||
|
state_.agenda_index = 0;
|
||||||
|
refresh_agenda();
|
||||||
|
return;
|
||||||
|
case Action::select:
|
||||||
|
choose_linear_item();
|
||||||
|
return;
|
||||||
|
case Action::back:
|
||||||
|
case Action::linear_view:
|
||||||
|
close_linear_view();
|
||||||
|
return;
|
||||||
|
case Action::search:
|
||||||
|
begin_search();
|
||||||
|
return;
|
||||||
|
case Action::calendars:
|
||||||
|
begin_calendar_picker();
|
||||||
|
return;
|
||||||
|
case Action::quit:
|
||||||
|
running_ = false;
|
||||||
|
return;
|
||||||
|
case Action::none:
|
||||||
|
case Action::toggle_help:
|
||||||
|
case Action::add_event:
|
||||||
|
case Action::edit_event:
|
||||||
|
case Action::delete_event:
|
||||||
|
case Action::undo:
|
||||||
|
case Action::redo:
|
||||||
|
case Action::agenda:
|
||||||
case Action::sync:
|
case Action::sync:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -903,6 +999,7 @@ void TuiApp::dispatch(const Action action) {
|
|||||||
case Action::delete_event:
|
case Action::delete_event:
|
||||||
case Action::undo:
|
case Action::undo:
|
||||||
case Action::redo:
|
case Action::redo:
|
||||||
|
case Action::linear_view:
|
||||||
case Action::sync:
|
case Action::sync:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -989,6 +1086,7 @@ void TuiApp::dispatch(const Action action) {
|
|||||||
case Action::next_month:
|
case Action::next_month:
|
||||||
case Action::today:
|
case Action::today:
|
||||||
case Action::toggle_help:
|
case Action::toggle_help:
|
||||||
|
case Action::linear_view:
|
||||||
case Action::sync:
|
case Action::sync:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1075,6 +1173,9 @@ void TuiApp::dispatch(const Action action) {
|
|||||||
case Action::agenda:
|
case Action::agenda:
|
||||||
begin_agenda();
|
begin_agenda();
|
||||||
return;
|
return;
|
||||||
|
case Action::linear_view:
|
||||||
|
begin_linear_view();
|
||||||
|
return;
|
||||||
case Action::calendars:
|
case Action::calendars:
|
||||||
begin_calendar_picker();
|
begin_calendar_picker();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -475,6 +475,112 @@ void test_agenda_input_and_search_precedence()
|
|||||||
"g toggles the agenda view");
|
"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
|
} // namespace
|
||||||
|
|
||||||
void test_help_frame()
|
void test_help_frame()
|
||||||
@@ -566,6 +672,8 @@ int main()
|
|||||||
test_agenda_empty_frame();
|
test_agenda_empty_frame();
|
||||||
test_agenda_selection_and_scrolling();
|
test_agenda_selection_and_scrolling();
|
||||||
test_agenda_input_and_search_precedence();
|
test_agenda_input_and_search_precedence();
|
||||||
|
test_linear_view_frame();
|
||||||
|
test_linear_view_empty_frame();
|
||||||
if (failures != 0) {
|
if (failures != 0) {
|
||||||
std::cerr << failures << " TUI test(s) failed\n";
|
std::cerr << failures << " TUI test(s) failed\n";
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
|
|||||||
Reference in New Issue
Block a user