1428 lines
45 KiB
C
1428 lines
45 KiB
C
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <ncurses.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <lardon3d/import_task.h>
|
|
#include <lardon3d/image_catalog.h>
|
|
#include <lardon3d/image_view.h>
|
|
#include <lardon3d/layout.h>
|
|
#include <lardon3d/project.h>
|
|
#include <lardon3d/runtime_observer.h>
|
|
#include <lardon3d/runtime_session.h>
|
|
#include <lardon3d/tui.h>
|
|
#include <lardon3d/tui_model.h>
|
|
#include <lardon3d/tui_optics.h>
|
|
#include <lardon3d/tui_ssd_async.h>
|
|
|
|
enum {
|
|
MINIMUM_ROWS = 15,
|
|
MINIMUM_COLUMNS = 60,
|
|
};
|
|
|
|
typedef enum {
|
|
INPUT_NONE = 0,
|
|
INPUT_PROJECT_CREATE,
|
|
INPUT_PROJECT_OPEN,
|
|
INPUT_IMPORT_DIRECTORY,
|
|
INPUT_IMAGE_FILTER,
|
|
INPUT_OPTICS_CREATE_BODY,
|
|
INPUT_OPTICS_CREATE_LENS,
|
|
INPUT_OPTICS_CREATE_CONFIGURATION,
|
|
INPUT_OPTICS_INSPECT_CAPTURE,
|
|
INPUT_OPTICS_ASSIGN_CAPTURE,
|
|
INPUT_OPTICS_ASSIGN_GROUP,
|
|
INPUT_OPTICS_LOOKUP_METADATA,
|
|
} InputMode;
|
|
|
|
typedef struct {
|
|
InputMode mode;
|
|
char text[PATH_MAX];
|
|
size_t length;
|
|
} TuiInput;
|
|
|
|
typedef struct {
|
|
Lardon3DRuntimeObserver *observer;
|
|
Lardon3DRuntimeSnapshot observation;
|
|
Lardon3DTuiSsdAsync *ssd_operation;
|
|
Lardon3DTuiSsdAsyncSnapshot ssd_operation_snapshot;
|
|
Lardon3DTuiOptics *optics;
|
|
Lardon3DTuiOpticsSnapshot optics_snapshot;
|
|
Lardon3DProjectDb *optics_database;
|
|
bool optics_retry_requested;
|
|
Lardon3DTuiPalette palette;
|
|
size_t selected_task;
|
|
bool fatal_error;
|
|
} TuiRuntime;
|
|
|
|
static size_t
|
|
catalog_page_size(void)
|
|
{
|
|
int rows;
|
|
int columns;
|
|
getmaxyx(stdscr, rows, columns);
|
|
(void)columns;
|
|
return rows > 18 ? (size_t)(rows - 18) : 1;
|
|
}
|
|
|
|
static void
|
|
normalize_view_navigation(Lardon3DAppState *state)
|
|
{
|
|
lardon3d_image_view_normalize(state->image_view, catalog_page_size());
|
|
}
|
|
|
|
static bool
|
|
reload_catalog(Lardon3DAppState *state, bool announce_success)
|
|
{
|
|
Lardon3DImageSort sort = lardon3d_image_view_sort(state->image_view);
|
|
char filter[LARDON3D_IMAGE_FILTER_CAPACITY];
|
|
(void)snprintf(
|
|
filter,
|
|
sizeof(filter),
|
|
"%s",
|
|
lardon3d_image_view_filter(state->image_view)
|
|
);
|
|
size_t selected_catalog = 0;
|
|
bool preserve_selection = lardon3d_image_view_catalog_index(
|
|
state->image_view,
|
|
lardon3d_image_view_selection(state->image_view),
|
|
&selected_catalog
|
|
);
|
|
lardon3d_image_view_destroy(state->image_view);
|
|
state->image_view = NULL;
|
|
lardon3d_image_catalog_destroy(state->image_catalog);
|
|
state->image_catalog = NULL;
|
|
char message[sizeof(state->status_message)] = "";
|
|
state->image_catalog = lardon3d_image_catalog_load(
|
|
state,
|
|
message,
|
|
sizeof(message)
|
|
);
|
|
if (!state->image_catalog) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"%s",
|
|
message
|
|
);
|
|
return false;
|
|
}
|
|
char catalog_message[sizeof(message)];
|
|
(void)snprintf(catalog_message, sizeof(catalog_message), "%s", message);
|
|
char view_error[sizeof(message)];
|
|
state->image_view = lardon3d_image_view_create(state->image_catalog);
|
|
if (!state->image_view
|
|
|| !lardon3d_image_view_set_sort(state->image_view, sort)
|
|
|| !lardon3d_image_view_set_filter(
|
|
state->image_view,
|
|
filter,
|
|
view_error,
|
|
sizeof(view_error)
|
|
)) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Error: unable to build the image view."
|
|
);
|
|
lardon3d_image_view_destroy(state->image_view);
|
|
state->image_view = NULL;
|
|
return false;
|
|
}
|
|
if (preserve_selection) {
|
|
size_t count = lardon3d_image_view_count(state->image_view);
|
|
for (size_t index = 0; index < count; ++index) {
|
|
size_t catalog_index;
|
|
if (lardon3d_image_view_catalog_index(
|
|
state->image_view,
|
|
index,
|
|
&catalog_index
|
|
)
|
|
&& catalog_index == selected_catalog) {
|
|
lardon3d_image_view_select(
|
|
state->image_view,
|
|
index,
|
|
catalog_page_size()
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
normalize_view_navigation(state);
|
|
if (catalog_message[0]) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"%s",
|
|
catalog_message
|
|
);
|
|
return true;
|
|
}
|
|
if (announce_success) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Catalog reloaded: %zu image%s.",
|
|
lardon3d_image_view_count(state->image_view),
|
|
lardon3d_image_view_count(state->image_view) == 1 ? "" : "s"
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
sync_optics(Lardon3DAppState *state, TuiRuntime *runtime)
|
|
{
|
|
if (runtime->optics_database != state->project_db
|
|
|| runtime->optics_retry_requested) {
|
|
lardon3d_tui_optics_unbind(runtime->optics);
|
|
runtime->optics_database = state->project_db;
|
|
runtime->optics_retry_requested = false;
|
|
if (state->project_db) {
|
|
(void)lardon3d_tui_optics_bind(
|
|
runtime->optics, state->project_db);
|
|
}
|
|
}
|
|
(void)lardon3d_tui_optics_snapshot(
|
|
runtime->optics, &runtime->optics_snapshot);
|
|
}
|
|
|
|
static bool
|
|
reset_project_session(Lardon3DAppState *state, TuiRuntime *runtime)
|
|
{
|
|
/* Observers borrow Queue/DB owners and must disappear before the exact
|
|
* Queue-finished-callback -> DB-close boundary executes. */
|
|
lardon3d_tui_optics_unbind(runtime->optics);
|
|
runtime->optics_database = NULL;
|
|
runtime->optics_retry_requested = false;
|
|
lardon3d_runtime_observer_destroy(runtime->observer);
|
|
runtime->observer = NULL;
|
|
if (!lardon3d_runtime_project_boundary(
|
|
state, LARDON3D_TASK_QUEUE_PRODUCTION_CAPACITY)) {
|
|
runtime->fatal_error = true;
|
|
state->running = false;
|
|
return false;
|
|
}
|
|
runtime->observer = lardon3d_runtime_observer_create(
|
|
&state->hardware_profile, state->task_queue,
|
|
state->resource_governor);
|
|
if (!runtime->observer) {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"Error: unable to recreate the runtime observer.");
|
|
runtime->fatal_error = true;
|
|
state->running = false;
|
|
return false;
|
|
}
|
|
runtime->observation = (Lardon3DRuntimeSnapshot) {0};
|
|
runtime->selected_task = 0;
|
|
return true;
|
|
}
|
|
|
|
static const char *
|
|
input_label(InputMode mode)
|
|
{
|
|
switch (mode) {
|
|
case INPUT_PROJECT_OPEN:
|
|
return "Nom du dossier projet :";
|
|
case INPUT_IMPORT_DIRECTORY:
|
|
return "Dossier source :";
|
|
case INPUT_IMAGE_FILTER:
|
|
return "Filter:";
|
|
case INPUT_OPTICS_CREATE_BODY:
|
|
return "Body immutable: manufacturer|model|name";
|
|
case INPUT_OPTICS_CREATE_LENS:
|
|
return "Lens: interface|range|min_mm|max_mm|maker|model|name";
|
|
case INPUT_OPTICS_CREATE_CONFIGURATION:
|
|
return "Integer focal length in mm, or ? for explicit absence";
|
|
case INPUT_OPTICS_INSPECT_CAPTURE:
|
|
return "Capture ID to inspect:";
|
|
case INPUT_OPTICS_ASSIGN_CAPTURE:
|
|
return "Capture ID to assign:";
|
|
case INPUT_OPTICS_ASSIGN_GROUP:
|
|
return "Campaign Task ID:group ID :";
|
|
case INPUT_OPTICS_LOOKUP_METADATA:
|
|
return "make|model|lens_make|lens_model (exact; lens vide permis)";
|
|
case INPUT_PROJECT_CREATE:
|
|
case INPUT_NONE:
|
|
default:
|
|
return "Nom du nouveau projet :";
|
|
}
|
|
}
|
|
|
|
static void
|
|
merge_ssd_observation(
|
|
Lardon3DRuntimeSnapshot *observation,
|
|
const Lardon3DTuiSsdAsyncSnapshot *ssd
|
|
)
|
|
{
|
|
if (!observation || !ssd || !ssd->controller_snapshot_known) {
|
|
return;
|
|
}
|
|
observation->ssd_controller_available = true;
|
|
observation->ssd = ssd->controller_snapshot;
|
|
/* Physical identity/actions stay on the SSD panel. Governor usage is
|
|
* captured separately by RuntimeObserver from the registered copy and is
|
|
* never overwritten by an independently timed controller observation. */
|
|
}
|
|
|
|
static void
|
|
redraw(
|
|
Lardon3DAppState *state,
|
|
const TuiInput *input,
|
|
Lardon3DImportTask *task,
|
|
TuiRuntime *runtime
|
|
)
|
|
{
|
|
int rows;
|
|
int columns;
|
|
getmaxyx(stdscr, rows, columns);
|
|
(void)lardon3d_runtime_observer_refresh(
|
|
runtime->observer, state->project_loaded, false,
|
|
&runtime->observation);
|
|
if (runtime->selected_task >= runtime->observation.task_count) {
|
|
runtime->selected_task = runtime->observation.task_count > 0
|
|
? runtime->observation.task_count - 1 : 0;
|
|
}
|
|
if (runtime->ssd_operation) {
|
|
/* This schedules at most one coalesced worker call; no controller or
|
|
* D-Bus work executes on the ncurses thread. */
|
|
(void)lardon3d_tui_ssd_async_refresh(runtime->ssd_operation);
|
|
(void)lardon3d_tui_ssd_async_poll(
|
|
runtime->ssd_operation, &runtime->ssd_operation_snapshot);
|
|
merge_ssd_observation(
|
|
&runtime->observation, &runtime->ssd_operation_snapshot);
|
|
} else {
|
|
runtime->ssd_operation_snapshot =
|
|
(Lardon3DTuiSsdAsyncSnapshot) {0};
|
|
}
|
|
sync_optics(state, runtime);
|
|
|
|
Lardon3DImportTaskSnapshot snapshot;
|
|
const Lardon3DImportTaskSnapshot *displayed_snapshot = NULL;
|
|
if (task && lardon3d_import_task_snapshot(task, &snapshot)) {
|
|
displayed_snapshot = &snapshot;
|
|
}
|
|
lardon3d_layout_draw_runtime(
|
|
state,
|
|
input->mode != INPUT_NONE ? input->text : NULL,
|
|
input_label(input->mode),
|
|
displayed_snapshot,
|
|
&runtime->observation,
|
|
&runtime->ssd_operation_snapshot,
|
|
&runtime->optics_snapshot,
|
|
runtime->selected_task,
|
|
&runtime->palette,
|
|
input->mode != INPUT_NONE
|
|
? LARDON3D_TUI_INTERACTION_TEXT_INPUT
|
|
: (task ? LARDON3D_TUI_INTERACTION_IMPORT_RUNNING
|
|
: LARDON3D_TUI_INTERACTION_IDLE),
|
|
rows,
|
|
columns
|
|
);
|
|
(void)curs_set(
|
|
input->mode != INPUT_NONE
|
|
&& rows >= MINIMUM_ROWS && columns >= MINIMUM_COLUMNS ? 1 : 0);
|
|
}
|
|
|
|
static bool
|
|
is_optics_input(InputMode mode)
|
|
{
|
|
return mode >= INPUT_OPTICS_CREATE_BODY
|
|
&& mode <= INPUT_OPTICS_LOOKUP_METADATA;
|
|
}
|
|
|
|
static bool
|
|
parse_positive_u64(const char *text, uint64_t maximum, uint64_t *value)
|
|
{
|
|
if (value) {
|
|
*value = 0;
|
|
}
|
|
if (!text || !text[0] || text[0] == '-' || !value || maximum == 0) {
|
|
return false;
|
|
}
|
|
char *end = NULL;
|
|
errno = 0;
|
|
unsigned long long parsed = strtoull(text, &end, 10);
|
|
if (errno != 0 || !end || *end != '\0' || parsed == 0
|
|
|| parsed > maximum) {
|
|
return false;
|
|
}
|
|
*value = (uint64_t)parsed;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
parse_nonnegative_u32(const char *text, uint32_t *value)
|
|
{
|
|
if (value) {
|
|
*value = 0;
|
|
}
|
|
if (!text || !text[0] || text[0] == '-' || !value) {
|
|
return false;
|
|
}
|
|
char *end = NULL;
|
|
errno = 0;
|
|
unsigned long long parsed = strtoull(text, &end, 10);
|
|
if (errno != 0 || !end || *end != '\0' || parsed > UINT32_MAX) {
|
|
return false;
|
|
}
|
|
*value = (uint32_t)parsed;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
parse_focal_mm(const char *text, uint32_t *micrometres)
|
|
{
|
|
uint32_t millimetres;
|
|
if (!parse_nonnegative_u32(text, &millimetres) || millimetres == 0
|
|
|| millimetres > UINT32_MAX / 1000U) {
|
|
return false;
|
|
}
|
|
*micrometres = millimetres * 1000U;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
split_exact_fields(char *text, char **fields, size_t expected_count)
|
|
{
|
|
if (!text || !fields || expected_count == 0) {
|
|
return false;
|
|
}
|
|
size_t count = 1;
|
|
fields[0] = text;
|
|
for (char *character = text; *character; ++character) {
|
|
if (*character != '|') {
|
|
continue;
|
|
}
|
|
if (count >= expected_count) {
|
|
return false;
|
|
}
|
|
*character = '\0';
|
|
fields[count++] = character + 1;
|
|
}
|
|
return count == expected_count;
|
|
}
|
|
|
|
static bool
|
|
copy_optical_text(
|
|
char destination[LARDON3D_OPTICAL_TEXT_CAPACITY],
|
|
const char *source
|
|
)
|
|
{
|
|
int written = snprintf(destination, LARDON3D_OPTICAL_TEXT_CAPACITY,
|
|
"%s", source ? source : "");
|
|
return written >= 0 && written < LARDON3D_OPTICAL_TEXT_CAPACITY;
|
|
}
|
|
|
|
static bool
|
|
parse_lens_interface(
|
|
const char *text,
|
|
Lardon3DOpticalLensInterface *interface_kind
|
|
)
|
|
{
|
|
if (strcmp(text, "manual") == 0) {
|
|
*interface_kind = LARDON3D_OPTICAL_LENS_MANUAL;
|
|
} else if (strcmp(text, "electronic") == 0) {
|
|
*interface_kind = LARDON3D_OPTICAL_LENS_ELECTRONIC;
|
|
} else if (strcmp(text, "integrated") == 0) {
|
|
*interface_kind = LARDON3D_OPTICAL_LENS_INTEGRATED;
|
|
} else {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
parse_focal_range(
|
|
const char *text,
|
|
Lardon3DOpticalFocalRangeKind *range_kind
|
|
)
|
|
{
|
|
if (strcmp(text, "unknown") == 0) {
|
|
*range_kind = LARDON3D_OPTICAL_FOCAL_RANGE_UNKNOWN;
|
|
} else if (strcmp(text, "prime") == 0) {
|
|
*range_kind = LARDON3D_OPTICAL_FOCAL_RANGE_PRIME;
|
|
} else if (strcmp(text, "zoom") == 0) {
|
|
*range_kind = LARDON3D_OPTICAL_FOCAL_RANGE_ZOOM;
|
|
} else {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
copy_optics_message(Lardon3DAppState *state, TuiRuntime *runtime)
|
|
{
|
|
(void)lardon3d_tui_optics_snapshot(
|
|
runtime->optics, &runtime->optics_snapshot);
|
|
(void)snprintf(state->status_message, sizeof(state->status_message),
|
|
"%s", runtime->optics_snapshot.message);
|
|
}
|
|
|
|
static bool
|
|
complete_optics_input(
|
|
Lardon3DAppState *state,
|
|
TuiInput *input,
|
|
TuiRuntime *runtime
|
|
)
|
|
{
|
|
char copy[sizeof(input->text)];
|
|
(void)snprintf(copy, sizeof(copy), "%s", input->text);
|
|
bool success = false;
|
|
bool model_called = false;
|
|
|
|
if (!runtime->optics_database) {
|
|
(void)snprintf(state->status_message, sizeof(state->status_message),
|
|
"No Project DB bound; no optical identity is guessed.");
|
|
input->mode = INPUT_NONE;
|
|
return false;
|
|
}
|
|
|
|
if (input->mode == INPUT_OPTICS_CREATE_BODY) {
|
|
char *fields[3];
|
|
Lardon3DOpticalCameraBodyProfile body = {0};
|
|
if (split_exact_fields(copy, fields, 3)
|
|
&& copy_optical_text(body.manufacturer, fields[0])
|
|
&& copy_optical_text(body.model, fields[1])
|
|
&& copy_optical_text(body.name, fields[2])) {
|
|
model_called = true;
|
|
success = lardon3d_tui_optics_create_body(
|
|
runtime->optics, &body);
|
|
} else {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"Format body invalide: manufacturer|model|name.");
|
|
}
|
|
} else if (input->mode == INPUT_OPTICS_CREATE_LENS) {
|
|
char *fields[7];
|
|
Lardon3DOpticalLensProfile lens = {0};
|
|
uint32_t minimum_mm;
|
|
uint32_t maximum_mm;
|
|
bool parsed = split_exact_fields(copy, fields, 7)
|
|
&& parse_lens_interface(fields[0], &lens.interface_kind)
|
|
&& parse_focal_range(fields[1], &lens.focal_range_kind)
|
|
&& parse_nonnegative_u32(fields[2], &minimum_mm)
|
|
&& parse_nonnegative_u32(fields[3], &maximum_mm)
|
|
&& minimum_mm <= UINT32_MAX / 1000U
|
|
&& maximum_mm <= UINT32_MAX / 1000U
|
|
&& copy_optical_text(lens.manufacturer, fields[4])
|
|
&& copy_optical_text(lens.model, fields[5])
|
|
&& copy_optical_text(lens.name, fields[6]);
|
|
if (parsed) {
|
|
lens.minimum_focal_um = minimum_mm * 1000U;
|
|
lens.maximum_focal_um = maximum_mm * 1000U;
|
|
model_called = true;
|
|
success = lardon3d_tui_optics_create_lens(
|
|
runtime->optics, &lens);
|
|
} else {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"Format lens invalide; interface manual/electronic/integrated, range unknown/prime/zoom.");
|
|
}
|
|
} else if (input->mode == INPUT_OPTICS_CREATE_CONFIGURATION) {
|
|
bool has_focal = strcmp(copy, "?") != 0;
|
|
uint32_t focal_um = 0;
|
|
if (!has_focal || parse_focal_mm(copy, &focal_um)) {
|
|
model_called = true;
|
|
success = lardon3d_tui_optics_create_configuration(
|
|
runtime->optics, has_focal, focal_um);
|
|
} else {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"Invalid focal length: positive integer in millimetres or ?.");
|
|
}
|
|
} else if (input->mode == INPUT_OPTICS_INSPECT_CAPTURE
|
|
|| input->mode == INPUT_OPTICS_ASSIGN_CAPTURE) {
|
|
uint64_t capture_id;
|
|
if (parse_positive_u64(copy, INT64_MAX, &capture_id)) {
|
|
model_called = true;
|
|
success = input->mode == INPUT_OPTICS_INSPECT_CAPTURE
|
|
? lardon3d_tui_optics_inspect_capture(
|
|
runtime->optics, capture_id)
|
|
: lardon3d_tui_optics_assign_capture(
|
|
runtime->optics, capture_id);
|
|
} else {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"Capture ID invalide (SQLite INTEGER positif requis).");
|
|
}
|
|
} else if (input->mode == INPUT_OPTICS_ASSIGN_GROUP) {
|
|
char *separator = strchr(copy, ':');
|
|
uint64_t campaign_id;
|
|
uint64_t group_id;
|
|
if (separator && !strchr(separator + 1, ':')) {
|
|
*separator = '\0';
|
|
}
|
|
if (separator
|
|
&& parse_positive_u64(copy, INT64_MAX, &campaign_id)
|
|
&& parse_positive_u64(separator + 1, UINT32_MAX, &group_id)) {
|
|
model_called = true;
|
|
success = lardon3d_tui_optics_assign_campaign_group(
|
|
runtime->optics, campaign_id, (uint32_t)group_id);
|
|
} else {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"Format campagne invalide: TaskID:groupID avec IDs positifs.");
|
|
}
|
|
} else if (input->mode == INPUT_OPTICS_LOOKUP_METADATA) {
|
|
char *fields[4];
|
|
if (split_exact_fields(copy, fields, 4)) {
|
|
model_called = true;
|
|
success = lardon3d_tui_optics_lookup_exact_metadata(
|
|
runtime->optics, fields[0], fields[1], fields[2], fields[3]);
|
|
} else {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"Invalid metadata format: make|model|lens_make|lens_model.");
|
|
}
|
|
}
|
|
if (model_called) {
|
|
copy_optics_message(state, runtime);
|
|
}
|
|
input->mode = INPUT_NONE;
|
|
input->text[0] = '\0';
|
|
input->length = 0;
|
|
return success;
|
|
}
|
|
|
|
static bool
|
|
start_import_task(
|
|
Lardon3DAppState *state,
|
|
TuiInput *input,
|
|
Lardon3DImportTask **task
|
|
)
|
|
{
|
|
*task = lardon3d_import_task_create();
|
|
if (!*task) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Error: unable to create the import task."
|
|
);
|
|
input->mode = INPUT_NONE;
|
|
return false;
|
|
}
|
|
if (!lardon3d_import_task_start(*task, state, input->text)) {
|
|
Lardon3DImportTaskSnapshot snapshot;
|
|
if (lardon3d_import_task_snapshot(*task, &snapshot)
|
|
&& snapshot.message[0]) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"%s",
|
|
snapshot.message
|
|
);
|
|
}
|
|
lardon3d_import_task_destroy(*task);
|
|
*task = NULL;
|
|
input->mode = INPUT_NONE;
|
|
return false;
|
|
}
|
|
input->mode = INPUT_NONE;
|
|
return true;
|
|
}
|
|
|
|
static bool handle_ssd_key(
|
|
Lardon3DAppState *state,
|
|
TuiRuntime *runtime
|
|
);
|
|
|
|
static bool
|
|
handle_active_input(
|
|
Lardon3DAppState *state,
|
|
TuiInput *input,
|
|
Lardon3DImportTask **task,
|
|
TuiRuntime *runtime,
|
|
int key
|
|
)
|
|
{
|
|
if (key == KEY_RESIZE) {
|
|
return true;
|
|
}
|
|
if (key == KEY_F(10)) {
|
|
/* F10 remains reachable while a text field owns ordinary keystrokes.
|
|
* Discard the uncommitted field before switching screens so hidden
|
|
* input cannot later mutate a Project/optical workflow. */
|
|
*input = (TuiInput) {0};
|
|
return handle_ssd_key(state, runtime);
|
|
}
|
|
|
|
if (key == 27) {
|
|
const char *message = "Project creation cancelled.";
|
|
if (input->mode == INPUT_PROJECT_OPEN) {
|
|
message = "Project opening cancelled.";
|
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
|
message = "Import cancelled.";
|
|
} else if (input->mode == INPUT_IMAGE_FILTER) {
|
|
message = "Filter cancelled.";
|
|
} else if (is_optics_input(input->mode)) {
|
|
message = "Optics operation cancelled.";
|
|
}
|
|
input->mode = INPUT_NONE;
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"%s",
|
|
message
|
|
);
|
|
return true;
|
|
}
|
|
|
|
if (key == '\n' || key == '\r' || key == KEY_ENTER) {
|
|
if (is_optics_input(input->mode)) {
|
|
(void)complete_optics_input(state, input, runtime);
|
|
} else if (input->mode == INPUT_PROJECT_OPEN) {
|
|
if (reset_project_session(state, runtime)
|
|
&& lardon3d_project_open(state, input->text)) {
|
|
(void)reload_catalog(state, false);
|
|
}
|
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
|
(void)start_import_task(state, input, task);
|
|
} else if (input->mode == INPUT_IMAGE_FILTER) {
|
|
char message[sizeof(state->status_message)];
|
|
if (lardon3d_image_view_set_filter(
|
|
state->image_view,
|
|
input->text,
|
|
message,
|
|
sizeof(message)
|
|
)) {
|
|
normalize_view_navigation(state);
|
|
if (lardon3d_image_view_count(state->image_view) == 0) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"No image matches the filter."
|
|
);
|
|
} else {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Filter applied: %.230s",
|
|
input->text
|
|
);
|
|
}
|
|
} else {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"%s",
|
|
message
|
|
);
|
|
}
|
|
} else {
|
|
if (reset_project_session(state, runtime)
|
|
&& lardon3d_project_create(state, input->text)) {
|
|
(void)reload_catalog(state, false);
|
|
}
|
|
}
|
|
if (input->mode != INPUT_NONE) {
|
|
input->mode = INPUT_NONE;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (key == KEY_BACKSPACE || key == 127 || key == '\b') {
|
|
if (input->length > 0) {
|
|
--input->length;
|
|
input->text[input->length] = '\0';
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) {
|
|
size_t capacity = input->mode == INPUT_IMAGE_FILTER
|
|
? LARDON3D_IMAGE_FILTER_CAPACITY
|
|
: sizeof(input->text);
|
|
if (input->length + 1 >= capacity) {
|
|
if (input->mode == INPUT_IMAGE_FILTER) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Error: filter is too long."
|
|
);
|
|
return true;
|
|
}
|
|
if (is_optics_input(input->mode)) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Error: optics input is too long."
|
|
);
|
|
input->mode = INPUT_NONE;
|
|
input->text[0] = '\0';
|
|
input->length = 0;
|
|
return true;
|
|
}
|
|
if (input->mode == INPUT_PROJECT_OPEN) {
|
|
if (reset_project_session(state, runtime)
|
|
&& lardon3d_project_open(state, input->text)) {
|
|
(void)reload_catalog(state, false);
|
|
}
|
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Error: source path is too long."
|
|
);
|
|
} else {
|
|
if (reset_project_session(state, runtime)
|
|
&& lardon3d_project_create(state, input->text)) {
|
|
(void)reload_catalog(state, false);
|
|
}
|
|
}
|
|
input->mode = INPUT_NONE;
|
|
return true;
|
|
}
|
|
|
|
input->text[input->length] = (char)key;
|
|
++input->length;
|
|
input->text[input->length] = '\0';
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
begin_input(TuiInput *input, InputMode mode)
|
|
{
|
|
*input = (TuiInput) {
|
|
.mode = mode,
|
|
.text = "",
|
|
.length = 0,
|
|
};
|
|
}
|
|
|
|
static bool
|
|
handle_ssd_key(
|
|
Lardon3DAppState *state,
|
|
TuiRuntime *runtime
|
|
)
|
|
{
|
|
/* F10 is the persistent SSD control key, not merely navigation. Move to
|
|
* the state screen first, then act immediately only from the freshly
|
|
* polled, validated snapshot; UNKNOWN remains a no-action observation. */
|
|
state->screen = LARDON3D_SCREEN_SSD;
|
|
if (!runtime->ssd_operation) {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"SSD controller unavailable; state UNKNOWN, no action.");
|
|
return true;
|
|
}
|
|
(void)lardon3d_tui_ssd_async_poll(
|
|
runtime->ssd_operation, &runtime->ssd_operation_snapshot);
|
|
if (runtime->ssd_operation_snapshot.running) {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"SSD operation %s already running.",
|
|
lardon3d_tui_ssd_action_name(
|
|
runtime->ssd_operation_snapshot.action));
|
|
return true;
|
|
}
|
|
if (!runtime->ssd_operation_snapshot.controller_snapshot_known
|
|
|| !runtime->ssd_operation_snapshot.controller_snapshot_actionable) {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"SSD telemetry unknown/invalid; control disabled.");
|
|
return true;
|
|
}
|
|
Lardon3DTuiSsdAction action;
|
|
const Lardon3DSsdSnapshot *current =
|
|
&runtime->ssd_operation_snapshot.controller_snapshot;
|
|
if (!lardon3d_tui_ssd_action_for_snapshot(current, &action)) {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"No safe SSD transition from %s.",
|
|
lardon3d_ssd_state_name(current->state));
|
|
return true;
|
|
}
|
|
if (!lardon3d_tui_ssd_async_request(runtime->ssd_operation, action)) {
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message),
|
|
"Unable to start the bounded SSD operation.");
|
|
return true;
|
|
}
|
|
(void)snprintf(state->status_message, sizeof(state->status_message),
|
|
"SSD %s started outside the ncurses thread.",
|
|
lardon3d_tui_ssd_action_name(action));
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_task_key(
|
|
Lardon3DAppState *state,
|
|
TuiRuntime *runtime,
|
|
int key
|
|
)
|
|
{
|
|
if (key == KEY_UP || key == 'k') {
|
|
if (runtime->selected_task > 0) {
|
|
--runtime->selected_task;
|
|
}
|
|
return true;
|
|
}
|
|
if (key == KEY_DOWN || key == 'j') {
|
|
if (runtime->selected_task + 1 < runtime->observation.task_count) {
|
|
++runtime->selected_task;
|
|
}
|
|
return true;
|
|
}
|
|
if (runtime->selected_task >= runtime->observation.task_count) {
|
|
return false;
|
|
}
|
|
uint64_t task_id = runtime->observation.tasks[
|
|
runtime->selected_task].id;
|
|
bool result = false;
|
|
const char *operation = NULL;
|
|
if (key == 'p' || key == 'P') {
|
|
operation = "pause";
|
|
result = lardon3d_task_queue_pause(state->task_queue, task_id);
|
|
} else if (key == 'r' || key == 'R') {
|
|
operation = "reprise";
|
|
result = lardon3d_task_queue_resume(state->task_queue, task_id);
|
|
} else if (key == 'c' || key == 'C') {
|
|
operation = "cancellation";
|
|
result = lardon3d_task_queue_cancel(state->task_queue, task_id);
|
|
}
|
|
if (!operation) {
|
|
return false;
|
|
}
|
|
(void)snprintf(state->status_message, sizeof(state->status_message),
|
|
"Task #%llu: %s %s.", (unsigned long long)task_id,
|
|
operation, result ? "requested" : "rejected/unavailable");
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
handle_optics_key(
|
|
Lardon3DAppState *state,
|
|
TuiInput *input,
|
|
TuiRuntime *runtime,
|
|
int key
|
|
)
|
|
{
|
|
bool handled = true;
|
|
if (key == '\t') {
|
|
Lardon3DTuiOpticsPane next = (Lardon3DTuiOpticsPane)(
|
|
((int)runtime->optics_snapshot.active_pane + 1)
|
|
% ((int)LARDON3D_TUI_OPTICS_PANE_CALIBRATION + 1));
|
|
(void)lardon3d_tui_optics_select_pane(runtime->optics, next);
|
|
} else if (key == KEY_UP || key == 'k') {
|
|
(void)lardon3d_tui_optics_move_selection(runtime->optics, -1);
|
|
} else if (key == KEY_DOWN || key == 'j') {
|
|
(void)lardon3d_tui_optics_move_selection(runtime->optics, 1);
|
|
} else if (key == '[' || key == ']') {
|
|
(void)lardon3d_tui_optics_page(runtime->optics,
|
|
runtime->optics_snapshot.active_pane, key == ']');
|
|
} else if (key == 'B') {
|
|
begin_input(input, INPUT_OPTICS_CREATE_BODY);
|
|
} else if (key == 'L') {
|
|
begin_input(input, INPUT_OPTICS_CREATE_LENS);
|
|
} else if (key == 'C') {
|
|
begin_input(input, INPUT_OPTICS_CREATE_CONFIGURATION);
|
|
} else if (key == 'V') {
|
|
begin_input(input, INPUT_OPTICS_INSPECT_CAPTURE);
|
|
} else if (key == 'A') {
|
|
begin_input(input, INPUT_OPTICS_ASSIGN_CAPTURE);
|
|
} else if (key == 'G') {
|
|
begin_input(input, INPUT_OPTICS_ASSIGN_GROUP);
|
|
} else if (key == 'E') {
|
|
begin_input(input, INPUT_OPTICS_LOOKUP_METADATA);
|
|
} else if (key == 'K') {
|
|
(void)lardon3d_tui_optics_select_capture_calibration(
|
|
runtime->optics);
|
|
copy_optics_message(state, runtime);
|
|
} else if (key == 'R') {
|
|
runtime->optics_retry_requested = true;
|
|
sync_optics(state, runtime);
|
|
copy_optics_message(state, runtime);
|
|
} else {
|
|
handled = false;
|
|
}
|
|
(void)lardon3d_tui_optics_snapshot(
|
|
runtime->optics, &runtime->optics_snapshot);
|
|
return handled;
|
|
}
|
|
|
|
static bool
|
|
handle_normal_input(
|
|
Lardon3DAppState *state,
|
|
TuiInput *input,
|
|
Lardon3DImportTask *task,
|
|
TuiRuntime *runtime,
|
|
int key
|
|
)
|
|
{
|
|
if (key == KEY_F(10)) {
|
|
return handle_ssd_key(state, runtime);
|
|
}
|
|
if (task) {
|
|
if (key == 'x' || key == 'X') {
|
|
lardon3d_import_task_request_cancel(task);
|
|
} else if (key == 'q' || key == 'Q') {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Quit is disabled during import."
|
|
);
|
|
} else if (key == 27) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Accueil indisponible pendant l'import."
|
|
);
|
|
}
|
|
return key == KEY_RESIZE || key == 'x' || key == 'X'
|
|
|| key == 'q' || key == 'Q' || key == 27;
|
|
}
|
|
if (key == 'q' || key == 'Q') {
|
|
state->running = false;
|
|
return false;
|
|
}
|
|
if (state->screen == LARDON3D_SCREEN_TASKS
|
|
&& handle_task_key(state, runtime, key)) {
|
|
return true;
|
|
}
|
|
if (state->screen == LARDON3D_SCREEN_OPTICS
|
|
&& handle_optics_key(state, input, runtime, key)) {
|
|
return true;
|
|
}
|
|
|
|
switch (key) {
|
|
case KEY_F(1):
|
|
state->screen = LARDON3D_SCREEN_HELP;
|
|
return true;
|
|
case KEY_F(2):
|
|
state->screen = LARDON3D_SCREEN_PROJECTS;
|
|
return true;
|
|
case KEY_F(3):
|
|
state->screen = LARDON3D_SCREEN_IMPORT;
|
|
return true;
|
|
case KEY_F(4):
|
|
state->screen = LARDON3D_SCREEN_VIEWER;
|
|
return true;
|
|
case KEY_F(5):
|
|
state->screen = LARDON3D_SCREEN_TASKS;
|
|
return true;
|
|
case KEY_F(6):
|
|
state->screen = LARDON3D_SCREEN_RESOURCES;
|
|
return true;
|
|
case KEY_F(7):
|
|
state->screen = LARDON3D_SCREEN_OPTICS;
|
|
return true;
|
|
case 27:
|
|
state->screen = LARDON3D_SCREEN_HOME;
|
|
return true;
|
|
case KEY_RESIZE:
|
|
normalize_view_navigation(state);
|
|
return true;
|
|
case KEY_UP:
|
|
case 'k':
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT
|
|
&& lardon3d_image_view_selection(state->image_view) > 0) {
|
|
lardon3d_image_view_select(
|
|
state->image_view,
|
|
lardon3d_image_view_selection(state->image_view) - 1,
|
|
catalog_page_size()
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
case KEY_DOWN:
|
|
case 'j': {
|
|
size_t count = lardon3d_image_view_count(state->image_view);
|
|
size_t selection = lardon3d_image_view_selection(state->image_view);
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT
|
|
&& selection < count && selection + 1 < count) {
|
|
lardon3d_image_view_select(
|
|
state->image_view,
|
|
selection + 1,
|
|
catalog_page_size()
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
case KEY_PPAGE:
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
|
size_t page_size = catalog_page_size();
|
|
size_t selection = lardon3d_image_view_selection(state->image_view);
|
|
selection = selection > page_size
|
|
? selection - page_size
|
|
: 0;
|
|
lardon3d_image_view_select(state->image_view, selection, page_size);
|
|
return true;
|
|
}
|
|
return false;
|
|
case KEY_NPAGE:
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
|
size_t count = lardon3d_image_view_count(state->image_view);
|
|
if (count > 0) {
|
|
size_t selection = lardon3d_image_view_selection(
|
|
state->image_view
|
|
);
|
|
size_t remaining = count - 1 - selection;
|
|
size_t page_size = catalog_page_size();
|
|
selection += remaining < page_size
|
|
? remaining
|
|
: page_size;
|
|
lardon3d_image_view_select(
|
|
state->image_view,
|
|
selection,
|
|
page_size
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
case KEY_HOME:
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
|
lardon3d_image_view_select(
|
|
state->image_view,
|
|
0,
|
|
catalog_page_size()
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
case KEY_END:
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
|
size_t count = lardon3d_image_view_count(state->image_view);
|
|
lardon3d_image_view_select(
|
|
state->image_view,
|
|
count > 0 ? count - 1 : 0,
|
|
catalog_page_size()
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
case 'n':
|
|
case 'N':
|
|
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
|
*input = (TuiInput) {
|
|
.mode = INPUT_PROJECT_CREATE,
|
|
.text = "",
|
|
.length = 0,
|
|
};
|
|
return true;
|
|
}
|
|
return false;
|
|
case 'o':
|
|
case 'O':
|
|
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
|
*input = (TuiInput) {
|
|
.mode = INPUT_PROJECT_OPEN,
|
|
.text = "",
|
|
.length = 0,
|
|
};
|
|
return true;
|
|
}
|
|
return false;
|
|
case 'i':
|
|
case 'I':
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
|
if (!state->project_loaded) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"No project loaded."
|
|
);
|
|
return true;
|
|
}
|
|
*input = (TuiInput) {
|
|
.mode = INPUT_IMPORT_DIRECTORY,
|
|
.text = "",
|
|
.length = 0,
|
|
};
|
|
return true;
|
|
}
|
|
return false;
|
|
case 'r':
|
|
case 'R':
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
|
if (!state->project_loaded) {
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"No project loaded."
|
|
);
|
|
} else {
|
|
(void)reload_catalog(state, true);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
case 's':
|
|
case 'S':
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT && state->image_view) {
|
|
Lardon3DImageSort sort = lardon3d_image_view_sort(
|
|
state->image_view
|
|
);
|
|
sort = (Lardon3DImageSort)(
|
|
((int)sort + 1) % ((int)LARDON3D_IMAGE_SORT_SIZE_DESC + 1)
|
|
);
|
|
if (lardon3d_image_view_set_sort(state->image_view, sort)) {
|
|
normalize_view_navigation(state);
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Tri : %s.",
|
|
lardon3d_image_view_sort_name(sort)
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
case '/':
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT && state->image_view) {
|
|
const char *filter = lardon3d_image_view_filter(state->image_view);
|
|
*input = (TuiInput) {
|
|
.mode = INPUT_IMAGE_FILTER,
|
|
.text = "",
|
|
.length = strlen(filter),
|
|
};
|
|
(void)snprintf(input->text, sizeof(input->text), "%s", filter);
|
|
return true;
|
|
}
|
|
return false;
|
|
case 'x':
|
|
case 'X':
|
|
if (state->screen == LARDON3D_SCREEN_IMPORT && state->image_view) {
|
|
char message[sizeof(state->status_message)];
|
|
if (lardon3d_image_view_set_filter(
|
|
state->image_view,
|
|
"",
|
|
message,
|
|
sizeof(message)
|
|
)) {
|
|
normalize_view_navigation(state);
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"Filter cleared."
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
case 'c':
|
|
case 'C':
|
|
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
|
(void)reset_project_session(state, runtime);
|
|
return true;
|
|
}
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool
|
|
lardon3d_tui_init(void)
|
|
{
|
|
if (!initscr()) {
|
|
return false;
|
|
}
|
|
|
|
if (cbreak() == ERR || noecho() == ERR || keypad(stdscr, true) == ERR
|
|
|| curs_set(0) == ERR) {
|
|
endwin();
|
|
return false;
|
|
}
|
|
timeout(75);
|
|
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
initialize_palette(Lardon3DTuiPalette *palette)
|
|
{
|
|
if (!palette || !has_colors() || start_color() == ERR) {
|
|
lardon3d_tui_palette_plan(false, 0, palette);
|
|
return;
|
|
}
|
|
short background = use_default_colors() == OK ? -1 : COLOR_BLACK;
|
|
lardon3d_tui_palette_plan(true, COLOR_PAIRS, palette);
|
|
static const struct {
|
|
Lardon3DTuiSemantic semantic;
|
|
short foreground;
|
|
} colors[] = {
|
|
{LARDON3D_TUI_SEMANTIC_HEALTHY, COLOR_GREEN},
|
|
{LARDON3D_TUI_SEMANTIC_WARNING, COLOR_YELLOW},
|
|
{LARDON3D_TUI_SEMANTIC_ERROR, COLOR_RED},
|
|
{LARDON3D_TUI_SEMANTIC_GPU, COLOR_CYAN},
|
|
{LARDON3D_TUI_SEMANTIC_CPU, COLOR_BLUE},
|
|
{LARDON3D_TUI_SEMANTIC_SSD, COLOR_MAGENTA},
|
|
};
|
|
bool any_pair = false;
|
|
for (size_t index = 0; index < sizeof(colors) / sizeof(colors[0]);
|
|
++index) {
|
|
short pair = palette->color_pair[colors[index].semantic];
|
|
if (pair > 0 && init_pair(pair, colors[index].foreground,
|
|
background) == OK) {
|
|
any_pair = true;
|
|
} else {
|
|
palette->color_pair[colors[index].semantic] = 0;
|
|
}
|
|
}
|
|
palette->color_enabled = any_pair;
|
|
}
|
|
|
|
static void
|
|
destroy_runtime(TuiRuntime *runtime)
|
|
{
|
|
if (!runtime) {
|
|
return;
|
|
}
|
|
/* The DB borrow ends here. The SSD worker is separately borrowed from App
|
|
* and deliberately survives this local runtime until Queue teardown. */
|
|
lardon3d_tui_optics_unbind(runtime->optics);
|
|
runtime->optics_database = NULL;
|
|
lardon3d_tui_optics_destroy(runtime->optics);
|
|
runtime->optics = NULL;
|
|
/* The SSD operation is borrowed. App keeps its Governor registration alive
|
|
* until after Queue destroy has joined every Task and released every
|
|
* scratch lease; tearing it down in this local frame would invert that
|
|
* ownership boundary. */
|
|
runtime->ssd_operation = NULL;
|
|
lardon3d_runtime_observer_destroy(runtime->observer);
|
|
runtime->observer = NULL;
|
|
}
|
|
|
|
bool
|
|
lardon3d_tui_run_with_ssd_operation(
|
|
Lardon3DAppState *state,
|
|
Lardon3DTuiSsdAsync *ssd_operation
|
|
)
|
|
{
|
|
if (!state) {
|
|
return false;
|
|
}
|
|
|
|
TuiInput input = {0};
|
|
Lardon3DImportTask *task = NULL;
|
|
TuiRuntime runtime = {0};
|
|
runtime.observer = lardon3d_runtime_observer_create(
|
|
&state->hardware_profile, state->task_queue,
|
|
state->resource_governor);
|
|
runtime.optics = lardon3d_tui_optics_create();
|
|
runtime.ssd_operation = ssd_operation;
|
|
if (!runtime.observer || !runtime.optics) {
|
|
destroy_runtime(&runtime);
|
|
return false;
|
|
}
|
|
initialize_palette(&runtime.palette);
|
|
if (!lardon3d_runtime_observer_refresh(
|
|
runtime.observer, state->project_loaded, true,
|
|
&runtime.observation)) {
|
|
/* With no previous cache the observer returns an initialized UNKNOWN
|
|
* copy; later failures retain and mark the last bounded copy stale. */
|
|
if (!runtime.observation.status[0]) {
|
|
(void)snprintf(runtime.observation.status,
|
|
sizeof(runtime.observation.status),
|
|
"Runtime observation unavailable; values remain UNKNOWN");
|
|
}
|
|
(void)snprintf(state->status_message,
|
|
sizeof(state->status_message), "%s",
|
|
runtime.observation.status);
|
|
}
|
|
redraw(state, &input, task, &runtime);
|
|
|
|
bool success = true;
|
|
|
|
while (state->running) {
|
|
int key = getch();
|
|
|
|
bool should_redraw = task != NULL
|
|
|| state->screen == LARDON3D_SCREEN_HOME
|
|
|| state->screen == LARDON3D_SCREEN_TASKS
|
|
|| state->screen == LARDON3D_SCREEN_RESOURCES
|
|
|| state->screen == LARDON3D_SCREEN_SSD
|
|
|| state->screen == LARDON3D_SCREEN_OPTICS
|
|
|| runtime.ssd_operation_snapshot.running;
|
|
if (task && lardon3d_import_task_is_finished(task)) {
|
|
Lardon3DImportTaskSnapshot snapshot;
|
|
if (!lardon3d_import_task_join(task)
|
|
|| !lardon3d_import_task_snapshot(task, &snapshot)) {
|
|
lardon3d_import_task_destroy(task);
|
|
task = NULL;
|
|
success = false;
|
|
break;
|
|
}
|
|
(void)snprintf(
|
|
state->status_message,
|
|
sizeof(state->status_message),
|
|
"%s",
|
|
snapshot.message
|
|
);
|
|
if (snapshot.status == LARDON3D_IMPORT_TASK_SUCCEEDED) {
|
|
(void)reload_catalog(state, false);
|
|
}
|
|
lardon3d_import_task_destroy(task);
|
|
task = NULL;
|
|
should_redraw = true;
|
|
}
|
|
|
|
if (key != ERR) {
|
|
should_redraw = (input.mode != INPUT_NONE
|
|
? handle_active_input(state, &input, &task, &runtime, key)
|
|
: handle_normal_input(
|
|
state, &input, task, &runtime, key))
|
|
|| should_redraw;
|
|
}
|
|
|
|
if (should_redraw) {
|
|
redraw(state, &input, task, &runtime);
|
|
}
|
|
}
|
|
|
|
if (task) {
|
|
lardon3d_import_task_request_cancel(task);
|
|
if (!lardon3d_import_task_join(task)) {
|
|
lardon3d_import_task_destroy(task);
|
|
task = NULL;
|
|
success = false;
|
|
} else {
|
|
lardon3d_import_task_destroy(task);
|
|
task = NULL;
|
|
}
|
|
}
|
|
|
|
destroy_runtime(&runtime);
|
|
return success && !runtime.fatal_error;
|
|
}
|
|
|
|
bool
|
|
lardon3d_tui_run_with_ssd(
|
|
Lardon3DAppState *state,
|
|
Lardon3DSsdController *ssd_controller
|
|
)
|
|
{
|
|
Lardon3DTuiSsdAsync *operation = ssd_controller
|
|
? lardon3d_tui_ssd_async_create(ssd_controller) : NULL;
|
|
if (ssd_controller && !operation) {
|
|
return false;
|
|
}
|
|
bool success = lardon3d_tui_run_with_ssd_operation(state, operation);
|
|
if (operation
|
|
&& !lardon3d_tui_ssd_async_destroy_checked(&operation)) {
|
|
return false;
|
|
}
|
|
return success;
|
|
}
|
|
|
|
bool
|
|
lardon3d_tui_run(Lardon3DAppState *state)
|
|
{
|
|
return lardon3d_tui_run_with_ssd_operation(state, NULL);
|
|
}
|
|
|
|
void
|
|
lardon3d_tui_shutdown(void)
|
|
{
|
|
endwin();
|
|
}
|