563 lines
22 KiB
C
563 lines
22 KiB
C
/**
|
|
* @file app_shell.c
|
|
* @brief Pure state machinery shared by the Notcurses application shell.
|
|
*/
|
|
|
|
#include "trainlog/app_shell.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <utf8proc.h>
|
|
|
|
#include "trainlog/terminal.h"
|
|
#include "trainlog/presentation.h"
|
|
|
|
static void copy_id(char output[TRAINLOG_SHELL_STABLE_ID_CAPACITY],
|
|
const char *value)
|
|
{
|
|
(void)snprintf(output, TRAINLOG_SHELL_STABLE_ID_CAPACITY, "%s",
|
|
value != NULL ? value : "");
|
|
}
|
|
|
|
void trainlog_shell_layout_compute(int columns, int rows,
|
|
TrainlogShellLayout *layout)
|
|
{
|
|
int content_x;
|
|
if (layout == NULL) return;
|
|
(void)memset(layout, 0, sizeof(*layout));
|
|
layout->rows = rows;
|
|
layout->columns = columns;
|
|
layout->usable = columns >= TRAINLOG_SHELL_MIN_COLUMNS &&
|
|
rows >= TRAINLOG_SHELL_MIN_ROWS;
|
|
if (!layout->usable) return;
|
|
|
|
/* CONTRACT: chrome owns four rows on every valid geometry. Content and
|
|
* overlays can therefore never clip or overwrite the footer. */
|
|
layout->header = (TrainlogRect){0, 0, 2, columns};
|
|
layout->footer = (TrainlogRect){rows - 2, 0, 2, columns};
|
|
layout->sidebar_visible = columns >= 100 && rows >= 26;
|
|
layout->sidebar_expanded = columns >= 120 && rows >= 32;
|
|
content_x = layout->sidebar_visible ? 23 : 0;
|
|
if (layout->sidebar_visible) {
|
|
layout->sidebar = (TrainlogRect){2, 0, rows - 4,
|
|
TRAINLOG_SHELL_SIDEBAR_COLUMNS};
|
|
layout->separator = (TrainlogRect){2, 22, rows - 4, 1};
|
|
}
|
|
layout->content = (TrainlogRect){2, content_x, rows - 4,
|
|
columns - content_x};
|
|
}
|
|
|
|
TrainlogRect trainlog_shell_overlay_rect(const TrainlogShellLayout *layout,
|
|
int preferred_width,
|
|
int preferred_height)
|
|
{
|
|
TrainlogRect result = {0, 0, 0, 0};
|
|
int maximum_width;
|
|
int maximum_height;
|
|
if (layout == NULL || !layout->usable) return result;
|
|
maximum_width = layout->columns >= 100 ? 80 : layout->columns;
|
|
if (layout->columns == 100 && maximum_width > 74) maximum_width = 74;
|
|
if (layout->columns == 80 && maximum_width > 78) maximum_width = 78;
|
|
maximum_height = layout->content.height;
|
|
if (layout->columns >= 120 && maximum_height > 29) maximum_height = 29;
|
|
if (layout->columns == 100 && maximum_height > 24) maximum_height = 24;
|
|
result.width = preferred_width > 0 && preferred_width < maximum_width
|
|
? preferred_width : maximum_width;
|
|
result.height = preferred_height > 0 && preferred_height < maximum_height
|
|
? preferred_height : maximum_height;
|
|
result.x = (layout->columns - result.width) / 2;
|
|
result.y = layout->content.y +
|
|
(layout->content.height - result.height) / 2;
|
|
return result;
|
|
}
|
|
|
|
bool trainlog_shell_rect_contains(const TrainlogRect *outer,
|
|
const TrainlogRect *inner)
|
|
{
|
|
return outer != NULL && inner != NULL && inner->height >= 0 &&
|
|
inner->width >= 0 && inner->y >= outer->y && inner->x >= outer->x &&
|
|
inner->y + inner->height <= outer->y + outer->height &&
|
|
inner->x + inner->width <= outer->x + outer->width;
|
|
}
|
|
|
|
void trainlog_navigation_init(TrainlogNavigationState *navigation)
|
|
{
|
|
if (navigation == NULL) return;
|
|
(void)memset(navigation, 0, sizeof(*navigation));
|
|
navigation->current.route = TRAINLOG_ROUTE_HOME;
|
|
}
|
|
|
|
bool trainlog_navigation_open(TrainlogNavigationState *navigation,
|
|
TrainlogAppRoute route,
|
|
const char *stable_id)
|
|
{
|
|
if (navigation == NULL) return false;
|
|
if (navigation->current.route == route &&
|
|
strcmp(navigation->current.stable_id, stable_id != NULL ? stable_id : "") == 0)
|
|
return true;
|
|
if (navigation->back_count == TRAINLOG_SHELL_ROUTE_DEPTH) {
|
|
(void)memmove(&navigation->back[0], &navigation->back[1],
|
|
(TRAINLOG_SHELL_ROUTE_DEPTH - 1U) * sizeof(navigation->back[0]));
|
|
--navigation->back_count;
|
|
}
|
|
navigation->back[navigation->back_count++] = navigation->current;
|
|
navigation->current.route = route;
|
|
copy_id(navigation->current.stable_id, stable_id);
|
|
return true;
|
|
}
|
|
|
|
bool trainlog_navigation_back(TrainlogNavigationState *navigation)
|
|
{
|
|
if (navigation == NULL || navigation->back_count == 0U) return false;
|
|
navigation->current = navigation->back[--navigation->back_count];
|
|
return true;
|
|
}
|
|
|
|
void trainlog_actions_clear(TrainlogActionModel *model)
|
|
{
|
|
if (model != NULL) (void)memset(model, 0, sizeof(*model));
|
|
}
|
|
|
|
bool trainlog_actions_add(TrainlogActionModel *model,
|
|
const TrainlogAction *action)
|
|
{
|
|
size_t index;
|
|
if (model == NULL || action == NULL || action->identifier == NULL ||
|
|
action->label == NULL || model->count >= TRAINLOG_SHELL_ACTION_LIMIT)
|
|
return false;
|
|
/* INVARIANT: identifier is the action's stable identity. Keeping it unique
|
|
* prevents the footer and F7 registry from advertising the same semantic
|
|
* action twice when contextual and shell-wide contributors overlap. */
|
|
for (index = 0U; index < model->count; ++index)
|
|
if (strcmp(model->items[index].identifier, action->identifier) == 0)
|
|
return false;
|
|
model->items[model->count++] = *action;
|
|
return true;
|
|
}
|
|
|
|
const TrainlogAction *trainlog_actions_find_key(const TrainlogActionModel *model,
|
|
int key)
|
|
{
|
|
size_t index;
|
|
if (model == NULL) return NULL;
|
|
for (index = 0U; index < model->count; ++index) {
|
|
if (model->items[index].enabled && model->items[index].key == key)
|
|
return &model->items[index];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
const TrainlogAction *trainlog_actions_at_priority(const TrainlogActionModel *model,
|
|
size_t position)
|
|
{
|
|
const TrainlogAction *result = NULL;
|
|
size_t rank;
|
|
size_t index;
|
|
if (model == NULL) return NULL;
|
|
for (rank = 0U; rank <= position; ++rank) {
|
|
result = NULL;
|
|
for (index = 0U; index < model->count; ++index) {
|
|
const TrainlogAction *candidate = &model->items[index];
|
|
size_t prior = 0U;
|
|
size_t other;
|
|
if (!candidate->enabled) continue;
|
|
for (other = 0U; other < model->count; ++other) {
|
|
if (!model->items[other].enabled) continue;
|
|
if (model->items[other].priority < candidate->priority ||
|
|
(model->items[other].priority == candidate->priority &&
|
|
other < index)) ++prior;
|
|
}
|
|
if (prior == rank) { result = candidate; break; }
|
|
}
|
|
if (result == NULL || rank == position) break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void trainlog_overlays_init(TrainlogOverlayStack *stack)
|
|
{
|
|
if (stack != NULL) (void)memset(stack, 0, sizeof(*stack));
|
|
}
|
|
|
|
bool trainlog_overlays_push(TrainlogOverlayStack *stack,
|
|
TrainlogOverlayType type,
|
|
TrainlogFocusTarget restore_focus,
|
|
const char *restore_stable_id)
|
|
{
|
|
TrainlogOverlay *overlay;
|
|
if (stack == NULL || stack->count >= TRAINLOG_SHELL_OVERLAY_LIMIT) return false;
|
|
overlay = &stack->items[stack->count++];
|
|
(void)memset(overlay, 0, sizeof(*overlay));
|
|
overlay->type = type;
|
|
overlay->restore_focus = restore_focus;
|
|
copy_id(overlay->restore_stable_id, restore_stable_id);
|
|
return true;
|
|
}
|
|
|
|
const TrainlogOverlay *trainlog_overlays_top(const TrainlogOverlayStack *stack)
|
|
{
|
|
return stack != NULL && stack->count > 0U
|
|
? &stack->items[stack->count - 1U] : NULL;
|
|
}
|
|
|
|
bool trainlog_overlays_pop(TrainlogOverlayStack *stack,
|
|
TrainlogFocusTarget *restore_focus,
|
|
char restore_stable_id[TRAINLOG_SHELL_STABLE_ID_CAPACITY])
|
|
{
|
|
TrainlogOverlay *overlay;
|
|
if (stack == NULL || stack->count == 0U) return false;
|
|
overlay = &stack->items[--stack->count];
|
|
if (restore_focus != NULL) *restore_focus = overlay->restore_focus;
|
|
if (restore_stable_id != NULL)
|
|
copy_id(restore_stable_id, overlay->restore_stable_id);
|
|
(void)memset(overlay, 0, sizeof(*overlay));
|
|
return true;
|
|
}
|
|
|
|
void trainlog_list_init(TrainlogListState *list)
|
|
{
|
|
if (list != NULL) (void)memset(list, 0, sizeof(*list));
|
|
}
|
|
|
|
void trainlog_list_set_items(TrainlogListState *list,
|
|
const char *const *stable_ids,
|
|
size_t count,
|
|
size_t visible_rows,
|
|
bool total_known,
|
|
bool more_available)
|
|
{
|
|
size_t index;
|
|
size_t selected = 0U;
|
|
bool found = false;
|
|
if (list == NULL) return;
|
|
for (index = 0U; index < count && stable_ids != NULL; ++index) {
|
|
if (strcmp(list->selected_id, stable_ids[index]) == 0) {
|
|
selected = index; found = true; break;
|
|
}
|
|
}
|
|
if (!found && count > 0U) {
|
|
selected = list->selected_index < count ? list->selected_index : count - 1U;
|
|
copy_id(list->selected_id, stable_ids[selected]);
|
|
} else if (count == 0U) {
|
|
list->selected_id[0] = '\0'; selected = 0U;
|
|
}
|
|
list->selected_index = selected;
|
|
list->item_count = count;
|
|
list->visible_rows = visible_rows > 0U ? visible_rows : 1U;
|
|
list->total_known = total_known;
|
|
list->more_available = more_available;
|
|
if (selected < list->viewport_start) list->viewport_start = selected;
|
|
if (selected >= list->viewport_start + list->visible_rows)
|
|
list->viewport_start = selected - list->visible_rows + 1U;
|
|
if (count <= list->visible_rows) list->viewport_start = 0U;
|
|
else if (list->viewport_start > count - list->visible_rows)
|
|
list->viewport_start = count - list->visible_rows;
|
|
}
|
|
|
|
void trainlog_list_move(TrainlogListState *list,
|
|
const char *const *stable_ids,
|
|
int delta)
|
|
{
|
|
size_t next;
|
|
if (list == NULL || stable_ids == NULL || list->item_count == 0U) return;
|
|
if (delta < 0) {
|
|
size_t amount = (size_t)(-(long long)delta);
|
|
next = amount > list->selected_index ? 0U : list->selected_index - amount;
|
|
} else {
|
|
size_t amount = (size_t)delta;
|
|
next = amount > list->item_count - 1U - list->selected_index
|
|
? list->item_count - 1U : list->selected_index + amount;
|
|
}
|
|
list->selected_index = next;
|
|
copy_id(list->selected_id, stable_ids[next]);
|
|
if (next < list->viewport_start) list->viewport_start = next;
|
|
if (next >= list->viewport_start + list->visible_rows)
|
|
list->viewport_start = next - list->visible_rows + 1U;
|
|
}
|
|
|
|
void trainlog_shell_format_list_label(const char *value,
|
|
int maximum_cells,
|
|
char *output,
|
|
size_t output_capacity)
|
|
{
|
|
const char *input = value != NULL ? value : "";
|
|
size_t input_bytes = 0U;
|
|
size_t output_bytes = 0U;
|
|
int cells = 0;
|
|
bool truncated = false;
|
|
|
|
if (output == NULL || output_capacity == 0U) return;
|
|
output[0] = '\0';
|
|
if (maximum_cells <= 0) return;
|
|
|
|
/* INVARIANT: reserve one display cell for the truncation marker before
|
|
* copying a code point. This keeps labels valid UTF-8 and makes clipped
|
|
* stable-ID-backed rows visibly distinct from complete names. */
|
|
while (input[input_bytes] != '\0') {
|
|
utf8proc_int32_t codepoint;
|
|
utf8proc_ssize_t parsed = utf8proc_iterate(
|
|
(const utf8proc_uint8_t *)input + input_bytes, -1, &codepoint);
|
|
int width;
|
|
if (parsed <= 0) { parsed = 1; codepoint = (unsigned char)input[input_bytes]; }
|
|
width = utf8proc_charwidth(codepoint);
|
|
if (width < 0) width = 1;
|
|
if (cells + width > maximum_cells ||
|
|
output_bytes + (size_t)parsed >= output_capacity) {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
(void)memcpy(output + output_bytes, input + input_bytes, (size_t)parsed);
|
|
output_bytes += (size_t)parsed;
|
|
input_bytes += (size_t)parsed;
|
|
cells += width;
|
|
}
|
|
if (input[input_bytes] != '\0') truncated = true;
|
|
if (truncated) {
|
|
static const char ellipsis[] = "…";
|
|
while (output_bytes > 0U && cells >= maximum_cells) {
|
|
utf8proc_int32_t codepoint;
|
|
size_t start = output_bytes - 1U;
|
|
while (start > 0U && ((unsigned char)output[start] & 0xc0U) == 0x80U)
|
|
--start;
|
|
if (utf8proc_iterate((const utf8proc_uint8_t *)output + start,
|
|
(utf8proc_ssize_t)(output_bytes - start), &codepoint) > 0) {
|
|
int width = utf8proc_charwidth(codepoint);
|
|
cells -= width > 0 ? width : 1;
|
|
}
|
|
output_bytes = start;
|
|
}
|
|
if (output_bytes + sizeof(ellipsis) <= output_capacity) {
|
|
(void)memcpy(output + output_bytes, ellipsis, sizeof(ellipsis));
|
|
return;
|
|
}
|
|
}
|
|
output[output_bytes] = '\0';
|
|
}
|
|
|
|
void trainlog_search_init(TrainlogSearchState *search)
|
|
{
|
|
if (search == NULL) return;
|
|
(void)memset(search, 0, sizeof(*search));
|
|
}
|
|
|
|
bool trainlog_search_insert(TrainlogSearchState *search,
|
|
const char *utf8,
|
|
size_t length)
|
|
{
|
|
utf8proc_int32_t codepoint;
|
|
utf8proc_ssize_t parsed;
|
|
if (search == NULL || utf8 == NULL || length == 0U ||
|
|
search->bytes + length >= TRAINLOG_SHELL_SEARCH_CAPACITY) {
|
|
if (search != NULL) search->capacity_error = true;
|
|
return false;
|
|
}
|
|
parsed = utf8proc_iterate((const utf8proc_uint8_t *)utf8,
|
|
(utf8proc_ssize_t)length, &codepoint);
|
|
if (parsed <= 0 || (size_t)parsed != length || codepoint < 0x20 ||
|
|
codepoint == 0x7f) return false;
|
|
(void)memmove(search->text + search->cursor + length,
|
|
search->text + search->cursor, search->bytes - search->cursor + 1U);
|
|
(void)memcpy(search->text + search->cursor, utf8, length);
|
|
search->cursor += length;
|
|
search->bytes += length;
|
|
search->capacity_error = false;
|
|
return true;
|
|
}
|
|
|
|
static size_t next_grapheme_boundary(const char *text, size_t bytes, size_t start)
|
|
{
|
|
utf8proc_int32_t previous;
|
|
utf8proc_ssize_t parsed;
|
|
size_t cursor = start;
|
|
utf8proc_int32_t state = 0;
|
|
if (text == NULL || start >= bytes) return bytes;
|
|
parsed = utf8proc_iterate((const utf8proc_uint8_t *)text + cursor,
|
|
(utf8proc_ssize_t)(bytes - cursor), &previous);
|
|
if (parsed <= 0) return start + 1U;
|
|
cursor += (size_t)parsed;
|
|
while (cursor < bytes) {
|
|
utf8proc_int32_t current;
|
|
parsed = utf8proc_iterate((const utf8proc_uint8_t *)text + cursor,
|
|
(utf8proc_ssize_t)(bytes - cursor), ¤t);
|
|
if (parsed <= 0 || utf8proc_grapheme_break_stateful(previous, current,
|
|
&state) != 0) break;
|
|
previous = current;
|
|
cursor += (size_t)parsed;
|
|
}
|
|
return cursor;
|
|
}
|
|
|
|
static size_t previous_grapheme_boundary(const char *text, size_t bytes,
|
|
size_t cursor)
|
|
{
|
|
size_t previous = 0U;
|
|
size_t next = 0U;
|
|
while (next < cursor && next < bytes) {
|
|
previous = next;
|
|
next = next_grapheme_boundary(text, bytes, next);
|
|
}
|
|
return previous;
|
|
}
|
|
|
|
bool trainlog_search_backspace(TrainlogSearchState *search)
|
|
{
|
|
size_t start;
|
|
if (search == NULL || search->cursor == 0U) return false;
|
|
start = previous_grapheme_boundary(search->text, search->bytes,
|
|
search->cursor);
|
|
(void)memmove(search->text + start, search->text + search->cursor,
|
|
search->bytes - search->cursor + 1U);
|
|
search->bytes -= search->cursor - start;
|
|
search->cursor = start;
|
|
search->capacity_error = false;
|
|
return true;
|
|
}
|
|
|
|
void trainlog_search_home(TrainlogSearchState *search)
|
|
{
|
|
if (search != NULL) search->cursor = 0U;
|
|
}
|
|
|
|
void trainlog_search_end(TrainlogSearchState *search)
|
|
{
|
|
if (search != NULL) search->cursor = search->bytes;
|
|
}
|
|
|
|
void trainlog_search_left(TrainlogSearchState *search)
|
|
{
|
|
if (search == NULL || search->cursor == 0U) return;
|
|
search->cursor = previous_grapheme_boundary(search->text, search->bytes,
|
|
search->cursor);
|
|
}
|
|
|
|
void trainlog_search_right(TrainlogSearchState *search)
|
|
{
|
|
if (search == NULL || search->cursor >= search->bytes) return;
|
|
search->cursor = next_grapheme_boundary(search->text, search->bytes,
|
|
search->cursor);
|
|
}
|
|
|
|
bool trainlog_search_escape(TrainlogSearchState *search)
|
|
{
|
|
if (search == NULL) return false;
|
|
if (search->bytes > 0U) {
|
|
search->text[0] = '\0'; search->bytes = 0U; search->cursor = 0U;
|
|
search->capacity_error = false; return false;
|
|
}
|
|
search->open = false; search->focused = false; return true;
|
|
}
|
|
|
|
void trainlog_form_init(TrainlogFormField *field, const char *initial_value)
|
|
{
|
|
size_t length;
|
|
if (field == NULL) return;
|
|
(void)memset(field, 0, sizeof(*field));
|
|
length = initial_value != NULL ? strlen(initial_value) : 0U;
|
|
if (length > TRAINLOG_SHELL_SEARCH_CAPACITY - 1U)
|
|
length = TRAINLOG_SHELL_SEARCH_CAPACITY - 1U;
|
|
if (length > 0U) (void)memcpy(field->text, initial_value, length);
|
|
field->text[length] = '\0';
|
|
field->bytes = length;
|
|
field->cursor = length;
|
|
field->active = true;
|
|
}
|
|
|
|
TrainlogFormResult trainlog_form_handle(TrainlogFormField *field, int key)
|
|
{
|
|
TrainlogSearchState editor;
|
|
bool changed = false;
|
|
if (field == NULL || !field->active) return TRAINLOG_FORM_IGNORED;
|
|
if (key == TRAINLOG_KEY_ENTER || key == '\n') return TRAINLOG_FORM_SUBMIT;
|
|
if (key == TRAINLOG_KEY_ESCAPE || key == 27) return TRAINLOG_FORM_CANCEL;
|
|
if (key == TRAINLOG_KEY_TAB) return TRAINLOG_FORM_NEXT;
|
|
if (key == TRAINLOG_KEY_SHIFT_TAB) return TRAINLOG_FORM_PREVIOUS;
|
|
if (key == TRAINLOG_KEY_F6) return TRAINLOG_FORM_OPEN_NAVIGATION;
|
|
if (key == TRAINLOG_KEY_F7) return TRAINLOG_FORM_OPEN_ACTIONS;
|
|
(void)memset(&editor, 0, sizeof(editor));
|
|
(void)snprintf(editor.text, sizeof(editor.text), "%s", field->text);
|
|
editor.bytes = field->bytes;
|
|
editor.cursor = field->cursor;
|
|
editor.open = true;
|
|
editor.focused = true;
|
|
if (key == TRAINLOG_KEY_HOME) trainlog_search_home(&editor);
|
|
else if (key == TRAINLOG_KEY_END) trainlog_search_end(&editor);
|
|
else if (key == TRAINLOG_KEY_LEFT) trainlog_search_left(&editor);
|
|
else if (key == TRAINLOG_KEY_RIGHT) trainlog_search_right(&editor);
|
|
else if (key == TRAINLOG_KEY_BACKSPACE || key == TRAINLOG_KEY_DELETE)
|
|
changed = trainlog_search_backspace(&editor);
|
|
else if (key >= 0x20 && key <= 0x10ffff) {
|
|
char encoded[5] = "";
|
|
utf8proc_ssize_t bytes = utf8proc_encode_char((utf8proc_int32_t)key,
|
|
(utf8proc_uint8_t *)encoded);
|
|
if (bytes > 0) changed = trainlog_search_insert(&editor, encoded,
|
|
(size_t)bytes);
|
|
} else return TRAINLOG_FORM_IGNORED;
|
|
(void)snprintf(field->text, sizeof(field->text), "%s", editor.text);
|
|
field->bytes = editor.bytes;
|
|
field->cursor = editor.cursor;
|
|
field->capacity_error = editor.capacity_error;
|
|
return changed ? TRAINLOG_FORM_EDITED : TRAINLOG_FORM_IGNORED;
|
|
}
|
|
|
|
const char *trainlog_route_title(TrainlogAppRoute route)
|
|
{
|
|
static const char *const keys[TRAINLOG_ROUTE_COUNT] = {
|
|
"nav.home", "nav.sessions", "route.session.current",
|
|
"route.session.generator", "route.session.manual",
|
|
"route.session.completed", "route.session.detail",
|
|
"route.exercise.catalog", "route.exercise.detail",
|
|
"route.exercise.knowledge", "route.exercise.performance",
|
|
"route.exercise.max", "route.equipment.catalog",
|
|
"route.equipment.detail", "nav.statistics", "route.stats.training",
|
|
"route.stats.exercise", "route.stats.exercise.detail",
|
|
"route.stats.zones", "route.body", "route.body.detail",
|
|
"route.body.metric", "route.body.trends", "route.body.global",
|
|
"route.body.analytics", "route.max", "nav.sync", "nav.settings"
|
|
};
|
|
/* INVARIANT: enum order is the stable route identity; wording is resolved
|
|
* only at this presentation boundary. */
|
|
if (route >= TRAINLOG_ROUTE_HOME && route < TRAINLOG_ROUTE_COUNT)
|
|
return trainlog_presentation_text(keys[route]);
|
|
return "Trainlog";
|
|
}
|
|
|
|
TrainlogAppRoute trainlog_route_section(TrainlogAppRoute route)
|
|
{
|
|
if (route >= TRAINLOG_ROUTE_SESSIONS && route <= TRAINLOG_ROUTE_SESSION_DETAIL)
|
|
return TRAINLOG_ROUTE_SESSIONS;
|
|
if (route == TRAINLOG_ROUTE_EXERCISE_DETAIL ||
|
|
route == TRAINLOG_ROUTE_EXERCISE_KNOWLEDGE) return TRAINLOG_ROUTE_EXERCISES;
|
|
if (route == TRAINLOG_ROUTE_EXERCISE_PERFORMANCE ||
|
|
route == TRAINLOG_ROUTE_EXERCISE_MAX) return TRAINLOG_ROUTE_STATS;
|
|
if (route == TRAINLOG_ROUTE_EQUIPMENT_DETAIL) return TRAINLOG_ROUTE_EQUIPMENT;
|
|
if (route == TRAINLOG_ROUTE_STATS_TRAINING ||
|
|
route == TRAINLOG_ROUTE_STATS_EXERCISE ||
|
|
route == TRAINLOG_ROUTE_STATS_EXERCISE_DETAIL ||
|
|
route == TRAINLOG_ROUTE_STATS_ZONES ||
|
|
(route >= TRAINLOG_ROUTE_BODY && route <= TRAINLOG_ROUTE_BODY_ANALYTICS) ||
|
|
route == TRAINLOG_ROUTE_MAX) return TRAINLOG_ROUTE_STATS;
|
|
return route;
|
|
}
|
|
|
|
TrainlogLeaveDecision trainlog_shell_leave_decision(
|
|
const TrainlogDurabilityState *state,
|
|
bool explicit_discard)
|
|
{
|
|
if (state == NULL) return TRAINLOG_LEAVE_ALLOW;
|
|
if (explicit_discard) return state->session_draft || state->session_dirty ||
|
|
state->generator_configuration_dirty || state->generator_preview ||
|
|
state->generator_preview_dirty || state->transient_form_dirty
|
|
? TRAINLOG_LEAVE_CONFIRM_DISCARD : TRAINLOG_LEAVE_ALLOW;
|
|
if (state->generator_preview || state->generator_preview_dirty ||
|
|
state->generator_configuration_dirty || state->transient_form_dirty)
|
|
return TRAINLOG_LEAVE_CONFIRM_KEEP;
|
|
/* A session draft is owned in memory by the run and ordinary navigation
|
|
* suspends it. Only q/discard asks to erase it. */
|
|
return TRAINLOG_LEAVE_ALLOW;
|
|
}
|
|
|
|
void trainlog_shell_discard_transient(TrainlogDurabilityState *state)
|
|
{
|
|
if (state != NULL) (void)memset(state, 0, sizeof(*state));
|
|
}
|