lardon3d/src/tui.c
2026-08-06 21:05:02 +02:00

728 lines
21 KiB
C

#include <ctype.h>
#include <limits.h>
#include <ncurses.h>
#include <stdbool.h>
#include <stdio.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/resource_governor.h>
#include <lardon3d/resource_snapshot.h>
#include <lardon3d/task_queue.h>
#include <lardon3d/tui.h>
enum {
MINIMUM_ROWS = 20,
MINIMUM_COLUMNS = 72,
DISPLAYED_TASK_CAPACITY = 64,
};
typedef enum {
INPUT_NONE = 0,
INPUT_PROJECT_CREATE,
INPUT_PROJECT_OPEN,
INPUT_IMPORT_DIRECTORY,
INPUT_IMAGE_FILTER,
} InputMode;
typedef struct {
InputMode mode;
char text[PATH_MAX];
size_t length;
} TuiInput;
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),
"Erreur : impossible de construire la vue des images."
);
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),
"Catalogue rechargé : %zu image%s.",
lardon3d_image_view_count(state->image_view),
lardon3d_image_view_count(state->image_view) == 1 ? "" : "s"
);
}
return true;
}
static void
redraw(
const Lardon3DAppState *state,
const TuiInput *input,
Lardon3DImportTask *task
)
{
int rows;
int columns;
getmaxyx(stdscr, rows, columns);
const char *text = input->mode != INPUT_NONE ? input->text : NULL;
const char *label = "Nom du nouveau projet :";
if (input->mode == INPUT_PROJECT_OPEN) {
label = "Nom du dossier projet :";
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
label = "Dossier source :";
} else if (input->mode == INPUT_IMAGE_FILTER) {
label = "Filtre :";
}
Lardon3DImportTaskSnapshot snapshot;
const Lardon3DImportTaskSnapshot *displayed_snapshot = NULL;
if (task && lardon3d_import_task_snapshot(task, &snapshot)) {
displayed_snapshot = &snapshot;
}
Lardon3DTaskSnapshot task_snapshots[DISPLAYED_TASK_CAPACITY];
Lardon3DTaskQueueSummary task_summary;
size_t task_count = lardon3d_task_queue_snapshot(
state->task_queue,
task_snapshots,
DISPLAYED_TASK_CAPACITY,
&task_summary
);
Lardon3DResourceSnapshot resource_snapshot;
Lardon3DResourceAvailability resource_availability;
const Lardon3DResourceAvailability *displayed_resources = NULL;
if (state->screen == LARDON3D_SCREEN_RESOURCES
&& lardon3d_resource_snapshot_capture(
&state->hardware_profile,
&resource_snapshot,
NULL,
0
)
&& lardon3d_resource_governor_availability(
state->resource_governor,
&resource_snapshot,
&resource_availability
)) {
displayed_resources = &resource_availability;
}
lardon3d_layout_draw(
state,
text,
label,
displayed_snapshot,
task_snapshots,
task_count,
&task_summary,
displayed_resources,
rows,
columns
);
(void)curs_set(
input->mode != INPUT_NONE
&& rows >= MINIMUM_ROWS
&& columns >= MINIMUM_COLUMNS
? 1
: 0
);
}
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),
"Erreur : impossible de créer la tâche d'import."
);
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_active_input(
Lardon3DAppState *state,
TuiInput *input,
Lardon3DImportTask **task,
int key
)
{
if (key == KEY_RESIZE) {
return true;
}
if (key == 27) {
const char *message = "Création du projet annulée.";
if (input->mode == INPUT_PROJECT_OPEN) {
message = "Ouverture du projet annulée.";
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
message = "Import annulé.";
} else if (input->mode == INPUT_IMAGE_FILTER) {
message = "Filtre annulé.";
}
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 (input->mode == INPUT_PROJECT_OPEN) {
if (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),
"Aucune image ne correspond au filtre."
);
} else {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Filtre appliqué : %.230s",
input->text
);
}
} else {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"%s",
message
);
}
} else {
if (lardon3d_project_create(state, input->text)) {
(void)reload_catalog(state, false);
}
}
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),
"Erreur : filtre trop long."
);
return true;
}
if (input->mode == INPUT_PROJECT_OPEN) {
if (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),
"Erreur : chemin source trop long."
);
} else {
if (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 bool
handle_normal_input(
Lardon3DAppState *state,
TuiInput *input,
Lardon3DImportTask *task,
int key
)
{
if (task) {
if (key == 'c' || key == 'C') {
lardon3d_import_task_request_cancel(task);
} else if (key == 'q' || key == 'Q') {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Quitter est désactivé pendant l'import."
);
} else if (key == 27) {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Accueil indisponible pendant l'import."
);
}
return key == KEY_RESIZE || key == 'c' || key == 'C'
|| key == 'q' || key == 'Q' || key == 27;
}
if (key == 'q' || key == 'Q') {
state->running = false;
return false;
}
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 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),
"Aucun projet chargé."
);
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),
"Aucun projet chargé."
);
} 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),
"Filtre effacé."
);
}
return true;
}
return false;
case 'c':
case 'C':
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
lardon3d_project_close(state);
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;
}
bool
lardon3d_tui_run(Lardon3DAppState *state)
{
if (!state) {
return false;
}
TuiInput input = {0};
Lardon3DImportTask *task = NULL;
redraw(state, &input, task);
while (state->running) {
int key = getch();
bool should_redraw = task != NULL
|| state->screen == LARDON3D_SCREEN_TASKS
|| state->screen == LARDON3D_SCREEN_RESOURCES;
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);
return false;
}
(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, key)
: handle_normal_input(state, &input, task, key))
|| should_redraw;
}
if (should_redraw) {
redraw(state, &input, task);
}
}
if (task) {
lardon3d_import_task_request_cancel(task);
if (!lardon3d_import_task_join(task)) {
lardon3d_import_task_destroy(task);
return false;
}
lardon3d_import_task_destroy(task);
}
return true;
}
void
lardon3d_tui_shutdown(void)
{
endwin();
}