feat: add in-memory image catalog
This commit is contained in:
parent
8bb90ed05a
commit
118172ad0e
10 changed files with 996 additions and 9 deletions
|
|
@ -35,3 +35,7 @@ le dossier choisi, sans parcourir ses sous-dossiers. Elles sont copiées vers
|
|||
`images/originals` et répertoriées dans `images/manifest.tsv`.
|
||||
L'import s'exécute en arrière-plan afin que la TUI reste réactive. Pendant une
|
||||
opération, la touche `C` demande son annulation.
|
||||
|
||||
L'écran Import présente un catalogue en mémoire vérifié par rapport au manifeste
|
||||
et aux fichiers de `images/originals`. Les flèches, `j`/`k`, PageUp, PageDown,
|
||||
Home et End naviguent dans la liste ; `R` recharge le catalogue.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
|
||||
#include <stdbool.h>
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct Lardon3DImageCatalog Lardon3DImageCatalog;
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_SCREEN_HOME = 0,
|
||||
|
|
@ -19,6 +22,9 @@ typedef struct {
|
|||
char project_name[128];
|
||||
char project_path[PATH_MAX];
|
||||
char status_message[256];
|
||||
Lardon3DImageCatalog *image_catalog;
|
||||
size_t image_selection;
|
||||
size_t image_offset;
|
||||
} Lardon3DAppState;
|
||||
|
||||
void lardon3d_app_state_init(Lardon3DAppState *state);
|
||||
|
|
|
|||
36
include/lardon3d/image_catalog.h
Normal file
36
include/lardon3d/image_catalog.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#ifndef LARDON3D_IMAGE_CATALOG_H
|
||||
#define LARDON3D_IMAGE_CATALOG_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <lardon3d/app_state.h>
|
||||
|
||||
typedef struct {
|
||||
char *filename;
|
||||
char *source_path;
|
||||
uint64_t size_bytes;
|
||||
} Lardon3DImageEntry;
|
||||
|
||||
Lardon3DImageCatalog *lardon3d_image_catalog_load(
|
||||
const Lardon3DAppState *state,
|
||||
char *error_message,
|
||||
size_t error_message_size
|
||||
);
|
||||
|
||||
void lardon3d_image_catalog_destroy(Lardon3DImageCatalog *catalog);
|
||||
size_t lardon3d_image_catalog_count(const Lardon3DImageCatalog *catalog);
|
||||
uint64_t lardon3d_image_catalog_total_size(
|
||||
const Lardon3DImageCatalog *catalog
|
||||
);
|
||||
const Lardon3DImageEntry *lardon3d_image_catalog_get(
|
||||
const Lardon3DImageCatalog *catalog,
|
||||
size_t index
|
||||
);
|
||||
void lardon3d_image_catalog_format_size(
|
||||
uint64_t size_bytes,
|
||||
char *text,
|
||||
size_t text_size
|
||||
);
|
||||
|
||||
#endif
|
||||
13
meson.build
13
meson.build
|
|
@ -31,6 +31,7 @@ executable(
|
|||
'src/layout.c',
|
||||
'src/import.c',
|
||||
'src/import_task.c',
|
||||
'src/image_catalog.c',
|
||||
'src/project.c',
|
||||
],
|
||||
include_directories: include_directories('include'),
|
||||
|
|
@ -62,3 +63,15 @@ import_task_test = executable(
|
|||
)
|
||||
|
||||
test('import-task', import_task_test, timeout: 30)
|
||||
|
||||
image_catalog_test = executable(
|
||||
'test-image-catalog',
|
||||
sources: [
|
||||
'tests/test_image_catalog.c',
|
||||
'src/app_state.c',
|
||||
'src/image_catalog.c',
|
||||
],
|
||||
include_directories: include_directories('include'),
|
||||
)
|
||||
|
||||
test('image-catalog', image_catalog_test, timeout: 30)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include <lardon3d/app.h>
|
||||
#include <lardon3d/app_state.h>
|
||||
#include <lardon3d/image_catalog.h>
|
||||
#include <lardon3d/tui.h>
|
||||
|
||||
int
|
||||
|
|
@ -20,6 +21,7 @@ lardon3d_app_run(void)
|
|||
}
|
||||
|
||||
bool success = lardon3d_tui_run(&state);
|
||||
lardon3d_image_catalog_destroy(state.image_catalog);
|
||||
lardon3d_tui_shutdown();
|
||||
|
||||
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
|
|
|
|||
393
src/image_catalog.c
Normal file
393
src/image_catalog.c
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <lardon3d/image_catalog.h>
|
||||
|
||||
struct Lardon3DImageCatalog {
|
||||
Lardon3DImageEntry *entries;
|
||||
size_t count;
|
||||
size_t capacity;
|
||||
uint64_t total_size;
|
||||
};
|
||||
|
||||
static void
|
||||
set_error(char *message, size_t size, const char *text)
|
||||
{
|
||||
if (message && size > 0) {
|
||||
(void)snprintf(message, size, "%s", text);
|
||||
}
|
||||
}
|
||||
|
||||
static char *
|
||||
join_path(const char *parent, const char *child)
|
||||
{
|
||||
size_t parent_length = strlen(parent);
|
||||
size_t child_length = strlen(child);
|
||||
if (parent_length > SIZE_MAX - child_length - 2) {
|
||||
return NULL;
|
||||
}
|
||||
size_t size = parent_length + child_length + 2;
|
||||
char *path = malloc(size);
|
||||
if (!path) {
|
||||
return NULL;
|
||||
}
|
||||
int written = snprintf(path, size, "%s/%s", parent, child);
|
||||
if (written < 0 || (size_t)written >= size) {
|
||||
free(path);
|
||||
return NULL;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_size(const char *text, uint64_t *value)
|
||||
{
|
||||
if (!text[0]) {
|
||||
return false;
|
||||
}
|
||||
uint64_t parsed = 0;
|
||||
for (const unsigned char *digit = (const unsigned char *)text;
|
||||
*digit;
|
||||
++digit) {
|
||||
if (*digit < '0' || *digit > '9') {
|
||||
return false;
|
||||
}
|
||||
uint64_t number = (uint64_t)(*digit - '0');
|
||||
if (parsed > (UINT64_MAX - number) / 10) {
|
||||
return false;
|
||||
}
|
||||
parsed = parsed * 10 + number;
|
||||
}
|
||||
*value = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
valid_filename(const char *filename)
|
||||
{
|
||||
return filename[0]
|
||||
&& !strchr(filename, '/')
|
||||
&& !strchr(filename, '\\')
|
||||
&& !strchr(filename, '\t')
|
||||
&& !strchr(filename, '\r')
|
||||
&& !strchr(filename, '\n');
|
||||
}
|
||||
|
||||
static bool
|
||||
filename_exists(
|
||||
const Lardon3DImageCatalog *catalog,
|
||||
const char *filename
|
||||
)
|
||||
{
|
||||
for (size_t index = 0; index < catalog->count; ++index) {
|
||||
if (strcmp(catalog->entries[index].filename, filename) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
append_entry(
|
||||
Lardon3DImageCatalog *catalog,
|
||||
const char *filename,
|
||||
const char *source_path,
|
||||
uint64_t size_bytes
|
||||
)
|
||||
{
|
||||
if (catalog->count == catalog->capacity) {
|
||||
size_t capacity = catalog->capacity == 0 ? 16 : catalog->capacity * 2;
|
||||
if (capacity < catalog->capacity
|
||||
|| capacity > SIZE_MAX / sizeof(*catalog->entries)) {
|
||||
return false;
|
||||
}
|
||||
void *entries = realloc(
|
||||
catalog->entries,
|
||||
capacity * sizeof(*catalog->entries)
|
||||
);
|
||||
if (!entries) {
|
||||
return false;
|
||||
}
|
||||
catalog->entries = entries;
|
||||
catalog->capacity = capacity;
|
||||
}
|
||||
|
||||
char *filename_copy = strdup(filename);
|
||||
char *source_copy = strdup(source_path);
|
||||
if (!filename_copy || !source_copy) {
|
||||
free(filename_copy);
|
||||
free(source_copy);
|
||||
return false;
|
||||
}
|
||||
catalog->entries[catalog->count] = (Lardon3DImageEntry) {
|
||||
.filename = filename_copy,
|
||||
.source_path = source_copy,
|
||||
.size_bytes = size_bytes,
|
||||
};
|
||||
++catalog->count;
|
||||
catalog->total_size += size_bytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
lardon3d_image_catalog_destroy(Lardon3DImageCatalog *catalog)
|
||||
{
|
||||
if (!catalog) {
|
||||
return;
|
||||
}
|
||||
for (size_t index = 0; index < catalog->count; ++index) {
|
||||
free(catalog->entries[index].filename);
|
||||
free(catalog->entries[index].source_path);
|
||||
}
|
||||
free(catalog->entries);
|
||||
free(catalog);
|
||||
}
|
||||
|
||||
static bool
|
||||
validate_file(
|
||||
const char *originals_path,
|
||||
const char *filename,
|
||||
uint64_t expected_size,
|
||||
char *error_message,
|
||||
size_t error_message_size
|
||||
)
|
||||
{
|
||||
char *path = join_path(originals_path, filename);
|
||||
if (!path) {
|
||||
set_error(error_message, error_message_size, "Erreur : chemin d'image trop long.");
|
||||
return false;
|
||||
}
|
||||
struct stat info;
|
||||
if (lstat(path, &info) != 0) {
|
||||
free(path);
|
||||
set_error(error_message, error_message_size, "Erreur : image du manifeste absente.");
|
||||
return false;
|
||||
}
|
||||
free(path);
|
||||
if (S_ISLNK(info.st_mode) || !S_ISREG(info.st_mode)) {
|
||||
set_error(error_message, error_message_size, "Erreur : image du manifeste non régulière.");
|
||||
return false;
|
||||
}
|
||||
if (info.st_size < 0 || (uintmax_t)info.st_size != (uintmax_t)expected_size) {
|
||||
set_error(error_message, error_message_size, "Erreur : taille d'image incohérente.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_line(
|
||||
Lardon3DImageCatalog *catalog,
|
||||
char *line,
|
||||
const char *originals_path,
|
||||
char *error_message,
|
||||
size_t error_message_size
|
||||
)
|
||||
{
|
||||
char *first_tab = strchr(line, '\t');
|
||||
char *second_tab = first_tab ? strchr(first_tab + 1, '\t') : NULL;
|
||||
if (!first_tab || !second_tab || strchr(second_tab + 1, '\t')) {
|
||||
set_error(error_message, error_message_size, "Erreur : ligne de manifeste invalide.");
|
||||
return false;
|
||||
}
|
||||
*first_tab = '\0';
|
||||
*second_tab = '\0';
|
||||
const char *filename = line;
|
||||
const char *size_text = first_tab + 1;
|
||||
const char *source_path = second_tab + 1;
|
||||
|
||||
uint64_t size_bytes;
|
||||
if (!valid_filename(filename)) {
|
||||
set_error(error_message, error_message_size, "Erreur : nom d'image invalide dans le manifeste.");
|
||||
return false;
|
||||
}
|
||||
if (!source_path[0]) {
|
||||
set_error(error_message, error_message_size, "Erreur : chemin source vide dans le manifeste.");
|
||||
return false;
|
||||
}
|
||||
if (!parse_size(size_text, &size_bytes)) {
|
||||
set_error(error_message, error_message_size, "Erreur : taille invalide dans le manifeste.");
|
||||
return false;
|
||||
}
|
||||
if (filename_exists(catalog, filename)) {
|
||||
set_error(error_message, error_message_size, "Erreur : image dupliquée dans le manifeste.");
|
||||
return false;
|
||||
}
|
||||
if (catalog->total_size > UINT64_MAX - size_bytes) {
|
||||
set_error(error_message, error_message_size, "Erreur : taille totale du catalogue trop grande.");
|
||||
return false;
|
||||
}
|
||||
if (!validate_file(
|
||||
originals_path,
|
||||
filename,
|
||||
size_bytes,
|
||||
error_message,
|
||||
error_message_size
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
if (!append_entry(catalog, filename, source_path, size_bytes)) {
|
||||
set_error(error_message, error_message_size, "Erreur : mémoire insuffisante pour le catalogue.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Lardon3DImageCatalog *
|
||||
lardon3d_image_catalog_load(
|
||||
const Lardon3DAppState *state,
|
||||
char *error_message,
|
||||
size_t error_message_size
|
||||
)
|
||||
{
|
||||
set_error(error_message, error_message_size, "");
|
||||
if (!state || !state->project_loaded || !state->project_path[0]) {
|
||||
set_error(error_message, error_message_size, "Aucun projet chargé.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Lardon3DImageCatalog *catalog = calloc(1, sizeof(*catalog));
|
||||
if (!catalog) {
|
||||
set_error(error_message, error_message_size, "Erreur : mémoire insuffisante pour le catalogue.");
|
||||
return NULL;
|
||||
}
|
||||
char *images_path = join_path(state->project_path, "images");
|
||||
char *originals_path = images_path ? join_path(images_path, "originals") : NULL;
|
||||
char *manifest_path = images_path ? join_path(images_path, "manifest.tsv") : NULL;
|
||||
if (!images_path || !originals_path || !manifest_path) {
|
||||
set_error(error_message, error_message_size, "Erreur : chemin du catalogue trop long.");
|
||||
goto failure;
|
||||
}
|
||||
|
||||
int descriptor = open(manifest_path, O_RDONLY | O_NOFOLLOW);
|
||||
if (descriptor < 0) {
|
||||
if (errno == ENOENT) {
|
||||
set_error(error_message, error_message_size, "Aucune image importée.");
|
||||
free(images_path);
|
||||
free(originals_path);
|
||||
free(manifest_path);
|
||||
return catalog;
|
||||
}
|
||||
set_error(error_message, error_message_size, "Erreur : impossible d'ouvrir manifest.tsv.");
|
||||
goto failure;
|
||||
}
|
||||
struct stat manifest_info;
|
||||
if (fstat(descriptor, &manifest_info) != 0 || !S_ISREG(manifest_info.st_mode)) {
|
||||
(void)close(descriptor);
|
||||
set_error(error_message, error_message_size, "Erreur : manifest.tsv n'est pas régulier.");
|
||||
goto failure;
|
||||
}
|
||||
FILE *file = fdopen(descriptor, "r");
|
||||
if (!file) {
|
||||
(void)close(descriptor);
|
||||
set_error(error_message, error_message_size, "Erreur : impossible de lire manifest.tsv.");
|
||||
goto failure;
|
||||
}
|
||||
|
||||
char *line = NULL;
|
||||
size_t capacity = 0;
|
||||
ssize_t length = getline(&line, &capacity, file);
|
||||
bool valid = length >= 0
|
||||
&& strcmp(line, "filename\tsize_bytes\tsource_path\n") == 0;
|
||||
if (!valid) {
|
||||
set_error(error_message, error_message_size, "Erreur : en-tête de manifest.tsv invalide.");
|
||||
}
|
||||
while (valid && (length = getline(&line, &capacity, file)) >= 0) {
|
||||
if (length == 0 || line[(size_t)length - 1] != '\n') {
|
||||
set_error(error_message, error_message_size, "Erreur : ligne tronquée dans manifest.tsv.");
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
line[(size_t)length - 1] = '\0';
|
||||
if (strchr(line, '\r') || strchr(line, '\n')
|
||||
|| !parse_line(
|
||||
catalog,
|
||||
line,
|
||||
originals_path,
|
||||
error_message,
|
||||
error_message_size
|
||||
)) {
|
||||
if (!error_message || !error_message_size || !error_message[0]) {
|
||||
set_error(error_message, error_message_size, "Erreur : ligne de manifeste invalide.");
|
||||
}
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if (ferror(file) || fclose(file) != 0) {
|
||||
set_error(error_message, error_message_size, "Erreur : lecture de manifest.tsv impossible.");
|
||||
valid = false;
|
||||
}
|
||||
free(line);
|
||||
free(images_path);
|
||||
free(originals_path);
|
||||
free(manifest_path);
|
||||
if (!valid) {
|
||||
lardon3d_image_catalog_destroy(catalog);
|
||||
return NULL;
|
||||
}
|
||||
return catalog;
|
||||
|
||||
failure:
|
||||
free(images_path);
|
||||
free(originals_path);
|
||||
free(manifest_path);
|
||||
lardon3d_image_catalog_destroy(catalog);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t
|
||||
lardon3d_image_catalog_count(const Lardon3DImageCatalog *catalog)
|
||||
{
|
||||
return catalog ? catalog->count : 0;
|
||||
}
|
||||
|
||||
uint64_t
|
||||
lardon3d_image_catalog_total_size(const Lardon3DImageCatalog *catalog)
|
||||
{
|
||||
return catalog ? catalog->total_size : 0;
|
||||
}
|
||||
|
||||
const Lardon3DImageEntry *
|
||||
lardon3d_image_catalog_get(
|
||||
const Lardon3DImageCatalog *catalog,
|
||||
size_t index
|
||||
)
|
||||
{
|
||||
return catalog && index < catalog->count ? &catalog->entries[index] : NULL;
|
||||
}
|
||||
|
||||
void
|
||||
lardon3d_image_catalog_format_size(
|
||||
uint64_t size_bytes,
|
||||
char *text,
|
||||
size_t text_size
|
||||
)
|
||||
{
|
||||
if (!text || text_size == 0) {
|
||||
return;
|
||||
}
|
||||
static const char *units[] = {"octets", "Kio", "Mio", "Gio"};
|
||||
long double value = (long double)size_bytes;
|
||||
size_t unit = 0;
|
||||
while (value >= 1024.0L && unit + 1 < sizeof(units) / sizeof(units[0])) {
|
||||
value /= 1024.0L;
|
||||
++unit;
|
||||
}
|
||||
if (unit == 0) {
|
||||
(void)snprintf(text, text_size, "%" PRIu64 " %s", size_bytes, units[unit]);
|
||||
} else {
|
||||
(void)snprintf(text, text_size, "%.1Lf %s", value, units[unit]);
|
||||
char *decimal = strchr(text, '.');
|
||||
if (decimal) {
|
||||
*decimal = ',';
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/layout.c
77
src/layout.c
|
|
@ -3,6 +3,7 @@
|
|||
#include <string.h>
|
||||
|
||||
#include <lardon3d/layout.h>
|
||||
#include <lardon3d/image_catalog.h>
|
||||
|
||||
enum {
|
||||
MINIMUM_ROWS = 20,
|
||||
|
|
@ -70,7 +71,7 @@ screen_texts(
|
|||
case LARDON3D_SCREEN_IMPORT:
|
||||
*title = "Import";
|
||||
*content = "Import des images";
|
||||
*footer = "I Importer des images ESC Accueil Q Quit";
|
||||
*footer = "I Importer R Recharger ↑/↓ Naviguer ESC Accueil Q Quit";
|
||||
break;
|
||||
case LARDON3D_SCREEN_VIEWER:
|
||||
*title = "Viewer";
|
||||
|
|
@ -131,11 +132,74 @@ draw_project_screen(
|
|||
draw_input_field(input_text, input_label, 11, columns);
|
||||
}
|
||||
|
||||
static void
|
||||
draw_catalog(
|
||||
const Lardon3DAppState *state,
|
||||
int rows,
|
||||
int columns
|
||||
)
|
||||
{
|
||||
if (!state->project_loaded) {
|
||||
draw_text(7, 4, columns - 6, "Aucun projet chargé.");
|
||||
return;
|
||||
}
|
||||
size_t count = lardon3d_image_catalog_count(state->image_catalog);
|
||||
char line[512];
|
||||
(void)snprintf(
|
||||
line,
|
||||
sizeof(line),
|
||||
"Images importées : %zu",
|
||||
count
|
||||
);
|
||||
draw_text(6, 4, columns - 6, line);
|
||||
char size_text[64];
|
||||
lardon3d_image_catalog_format_size(
|
||||
lardon3d_image_catalog_total_size(state->image_catalog),
|
||||
size_text,
|
||||
sizeof(size_text)
|
||||
);
|
||||
(void)snprintf(line, sizeof(line), "Taille totale : %s", size_text);
|
||||
draw_text(7, 4, columns - 6, line);
|
||||
if (count == 0) {
|
||||
draw_text(9, 4, columns - 6, "Aucune image importée.");
|
||||
return;
|
||||
}
|
||||
|
||||
int journal_row = rows - 7;
|
||||
size_t visible = journal_row > 9 ? (size_t)(journal_row - 9) : 0;
|
||||
for (size_t row = 0; row < visible; ++row) {
|
||||
size_t index = state->image_offset + row;
|
||||
const Lardon3DImageEntry *entry = lardon3d_image_catalog_get(
|
||||
state->image_catalog,
|
||||
index
|
||||
);
|
||||
if (!entry) {
|
||||
break;
|
||||
}
|
||||
lardon3d_image_catalog_format_size(
|
||||
entry->size_bytes,
|
||||
size_text,
|
||||
sizeof(size_text)
|
||||
);
|
||||
(void)snprintf(
|
||||
line,
|
||||
sizeof(line),
|
||||
"%c %s %s",
|
||||
index == state->image_selection ? '>' : ' ',
|
||||
entry->filename,
|
||||
size_text
|
||||
);
|
||||
draw_text(9 + (int)row, 4, columns - 6, line);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
draw_import_screen(
|
||||
const Lardon3DAppState *state,
|
||||
const char *input_text,
|
||||
const char *input_label,
|
||||
const Lardon3DImportTaskSnapshot *snapshot,
|
||||
int rows,
|
||||
int columns
|
||||
)
|
||||
{
|
||||
|
|
@ -191,10 +255,11 @@ draw_import_screen(
|
|||
draw_text(12, 4, columns - 6, "C : Annuler l'import");
|
||||
return;
|
||||
}
|
||||
draw_text(7, 4, columns - 6, "I : Importer des images");
|
||||
draw_text(8, 4, columns - 6, "ESC : Accueil");
|
||||
draw_text(9, 4, columns - 6, "Q : Quitter");
|
||||
draw_input_field(input_text, input_label, 10, columns);
|
||||
if (input_text) {
|
||||
draw_input_field(input_text, input_label, 7, columns);
|
||||
return;
|
||||
}
|
||||
draw_catalog(state, rows, columns);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -237,9 +302,11 @@ draw_content(
|
|||
);
|
||||
} else if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
||||
draw_import_screen(
|
||||
state,
|
||||
input_text,
|
||||
input_label,
|
||||
import_snapshot,
|
||||
rows,
|
||||
columns
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <unistd.h>
|
||||
|
||||
#include <lardon3d/project.h>
|
||||
#include <lardon3d/image_catalog.h>
|
||||
|
||||
enum {
|
||||
MAX_CREATED_DIRECTORIES = 16,
|
||||
|
|
@ -32,6 +33,15 @@ set_status(Lardon3DAppState *state, const char *message)
|
|||
);
|
||||
}
|
||||
|
||||
static void
|
||||
clear_catalog(Lardon3DAppState *state)
|
||||
{
|
||||
lardon3d_image_catalog_destroy(state->image_catalog);
|
||||
state->image_catalog = NULL;
|
||||
state->image_selection = 0;
|
||||
state->image_offset = 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
normalize_name(
|
||||
Lardon3DAppState *state,
|
||||
|
|
@ -346,6 +356,7 @@ lardon3d_project_create(Lardon3DAppState *state, const char *name)
|
|||
return false;
|
||||
}
|
||||
|
||||
clear_catalog(state);
|
||||
state->project_loaded = true;
|
||||
(void)copy_path(
|
||||
state->project_name,
|
||||
|
|
@ -495,6 +506,7 @@ lardon3d_project_open(
|
|||
return false;
|
||||
}
|
||||
|
||||
clear_catalog(state);
|
||||
state->project_loaded = true;
|
||||
(void)copy_path(
|
||||
state->project_name,
|
||||
|
|
@ -527,6 +539,7 @@ lardon3d_project_close(Lardon3DAppState *state)
|
|||
return;
|
||||
}
|
||||
|
||||
clear_catalog(state);
|
||||
state->project_loaded = false;
|
||||
state->project_name[0] = '\0';
|
||||
state->project_path[0] = '\0';
|
||||
|
|
|
|||
159
src/tui.c
159
src/tui.c
|
|
@ -5,6 +5,7 @@
|
|||
#include <stdio.h>
|
||||
|
||||
#include <lardon3d/import_task.h>
|
||||
#include <lardon3d/image_catalog.h>
|
||||
#include <lardon3d/layout.h>
|
||||
#include <lardon3d/project.h>
|
||||
#include <lardon3d/tui.h>
|
||||
|
|
@ -27,6 +28,70 @@ typedef struct {
|
|||
size_t length;
|
||||
} TuiInput;
|
||||
|
||||
static size_t
|
||||
catalog_page_size(void)
|
||||
{
|
||||
int rows;
|
||||
int columns;
|
||||
getmaxyx(stdscr, rows, columns);
|
||||
(void)columns;
|
||||
return rows > 16 ? (size_t)(rows - 16) : 1;
|
||||
}
|
||||
|
||||
static void
|
||||
normalize_catalog_navigation(Lardon3DAppState *state)
|
||||
{
|
||||
size_t count = lardon3d_image_catalog_count(state->image_catalog);
|
||||
if (count == 0) {
|
||||
state->image_selection = 0;
|
||||
state->image_offset = 0;
|
||||
return;
|
||||
}
|
||||
if (state->image_selection >= count) {
|
||||
state->image_selection = count - 1;
|
||||
}
|
||||
if (state->image_offset > state->image_selection) {
|
||||
state->image_offset = state->image_selection;
|
||||
}
|
||||
size_t page_size = catalog_page_size();
|
||||
if (state->image_selection - state->image_offset >= page_size) {
|
||||
state->image_offset = state->image_selection - page_size + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
reload_catalog(Lardon3DAppState *state, bool announce_success)
|
||||
{
|
||||
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)
|
||||
);
|
||||
normalize_catalog_navigation(state);
|
||||
if (!state->image_catalog || message[0]) {
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
"%s",
|
||||
message
|
||||
);
|
||||
return state->image_catalog != NULL;
|
||||
}
|
||||
if (announce_success) {
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
"Catalogue rechargé : %zu image%s.",
|
||||
lardon3d_image_catalog_count(state->image_catalog),
|
||||
lardon3d_image_catalog_count(state->image_catalog) == 1 ? "" : "s"
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
redraw(
|
||||
const Lardon3DAppState *state,
|
||||
|
|
@ -135,11 +200,15 @@ handle_active_input(
|
|||
|
||||
if (key == '\n' || key == '\r' || key == KEY_ENTER) {
|
||||
if (input->mode == INPUT_PROJECT_OPEN) {
|
||||
(void)lardon3d_project_open(state, input->text);
|
||||
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 {
|
||||
(void)lardon3d_project_create(state, input->text);
|
||||
if (lardon3d_project_create(state, input->text)) {
|
||||
(void)reload_catalog(state, false);
|
||||
}
|
||||
}
|
||||
input->mode = INPUT_NONE;
|
||||
return true;
|
||||
|
|
@ -156,7 +225,9 @@ handle_active_input(
|
|||
if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) {
|
||||
if (input->length + 1 >= sizeof(input->text)) {
|
||||
if (input->mode == INPUT_PROJECT_OPEN) {
|
||||
(void)lardon3d_project_open(state, input->text);
|
||||
if (lardon3d_project_open(state, input->text)) {
|
||||
(void)reload_catalog(state, false);
|
||||
}
|
||||
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
|
|
@ -164,7 +235,9 @@ handle_active_input(
|
|||
"Erreur : chemin source trop long."
|
||||
);
|
||||
} else {
|
||||
(void)lardon3d_project_create(state, input->text);
|
||||
if (lardon3d_project_create(state, input->text)) {
|
||||
(void)reload_catalog(state, false);
|
||||
}
|
||||
}
|
||||
input->mode = INPUT_NONE;
|
||||
return true;
|
||||
|
|
@ -228,7 +301,67 @@ handle_normal_input(
|
|||
state->screen = LARDON3D_SCREEN_HOME;
|
||||
return true;
|
||||
case KEY_RESIZE:
|
||||
normalize_catalog_navigation(state);
|
||||
return true;
|
||||
case KEY_UP:
|
||||
case 'k':
|
||||
if (state->screen == LARDON3D_SCREEN_IMPORT
|
||||
&& state->image_selection > 0) {
|
||||
--state->image_selection;
|
||||
normalize_catalog_navigation(state);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case KEY_DOWN:
|
||||
case 'j': {
|
||||
size_t count = lardon3d_image_catalog_count(state->image_catalog);
|
||||
if (state->screen == LARDON3D_SCREEN_IMPORT
|
||||
&& state->image_selection + 1 < count) {
|
||||
++state->image_selection;
|
||||
normalize_catalog_navigation(state);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case KEY_PPAGE:
|
||||
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
||||
size_t page_size = catalog_page_size();
|
||||
state->image_selection = state->image_selection > page_size
|
||||
? state->image_selection - page_size
|
||||
: 0;
|
||||
normalize_catalog_navigation(state);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case KEY_NPAGE:
|
||||
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
||||
size_t count = lardon3d_image_catalog_count(state->image_catalog);
|
||||
if (count > 0) {
|
||||
size_t remaining = count - 1 - state->image_selection;
|
||||
size_t page_size = catalog_page_size();
|
||||
state->image_selection += remaining < page_size
|
||||
? remaining
|
||||
: page_size;
|
||||
normalize_catalog_navigation(state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case KEY_HOME:
|
||||
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
||||
state->image_selection = 0;
|
||||
normalize_catalog_navigation(state);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case KEY_END:
|
||||
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
||||
size_t count = lardon3d_image_catalog_count(state->image_catalog);
|
||||
state->image_selection = count > 0 ? count - 1 : 0;
|
||||
normalize_catalog_navigation(state);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case 'n':
|
||||
case 'N':
|
||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||
|
|
@ -270,6 +403,21 @@ handle_normal_input(
|
|||
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 'c':
|
||||
case 'C':
|
||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||
|
|
@ -327,6 +475,9 @@ lardon3d_tui_run(Lardon3DAppState *state)
|
|||
"%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;
|
||||
|
|
|
|||
302
tests/test_image_catalog.c
Normal file
302
tests/test_image_catalog.c
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <lardon3d/app_state.h>
|
||||
#include <lardon3d/image_catalog.h>
|
||||
|
||||
#define CHECK(condition) \
|
||||
do { \
|
||||
if (!(condition)) { \
|
||||
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
|
||||
return false; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static bool
|
||||
join_path(char destination[PATH_MAX], const char *parent, const char *child)
|
||||
{
|
||||
int written = snprintf(destination, PATH_MAX, "%s/%s", parent, child);
|
||||
return written >= 0 && (size_t)written < PATH_MAX;
|
||||
}
|
||||
|
||||
static bool
|
||||
write_all(int descriptor, const char *content, size_t length)
|
||||
{
|
||||
size_t total = 0;
|
||||
while (total < length) {
|
||||
ssize_t written = write(descriptor, content + total, length - total);
|
||||
if (written < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (written <= 0) {
|
||||
return false;
|
||||
}
|
||||
total += (size_t)written;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
create_file(const char *path, const char *content)
|
||||
{
|
||||
int descriptor = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644);
|
||||
if (descriptor < 0) {
|
||||
return false;
|
||||
}
|
||||
bool success = write_all(descriptor, content, strlen(content));
|
||||
if (close(descriptor) != 0) {
|
||||
success = false;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool
|
||||
replace_manifest(const char *path, const char *content)
|
||||
{
|
||||
if (unlink(path) != 0 && errno != ENOENT) {
|
||||
return false;
|
||||
}
|
||||
return create_file(path, content);
|
||||
}
|
||||
|
||||
static bool
|
||||
remove_tree(const char *path)
|
||||
{
|
||||
struct stat info;
|
||||
if (lstat(path, &info) != 0) {
|
||||
return errno == ENOENT;
|
||||
}
|
||||
if (!S_ISDIR(info.st_mode)) {
|
||||
return unlink(path) == 0;
|
||||
}
|
||||
DIR *directory = opendir(path);
|
||||
if (!directory) {
|
||||
return false;
|
||||
}
|
||||
bool success = true;
|
||||
for (struct dirent *entry = readdir(directory);
|
||||
entry;
|
||||
entry = readdir(directory)) {
|
||||
if (strcmp(entry->d_name, ".") == 0
|
||||
|| strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
char child[PATH_MAX];
|
||||
if (!join_path(child, path, entry->d_name) || !remove_tree(child)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
if (closedir(directory) != 0 || rmdir(path) != 0) {
|
||||
success = false;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool
|
||||
expect_invalid(
|
||||
const Lardon3DAppState *state,
|
||||
const char *manifest,
|
||||
const char *content
|
||||
)
|
||||
{
|
||||
CHECK(replace_manifest(manifest, content));
|
||||
char error[256];
|
||||
Lardon3DImageCatalog *catalog = lardon3d_image_catalog_load(
|
||||
state,
|
||||
error,
|
||||
sizeof(error)
|
||||
);
|
||||
CHECK(!catalog);
|
||||
CHECK(error[0]);
|
||||
lardon3d_image_catalog_destroy(catalog);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
test_sizes(void)
|
||||
{
|
||||
char text[64];
|
||||
lardon3d_image_catalog_format_size(512, text, sizeof(text));
|
||||
CHECK(strcmp(text, "512 octets") == 0);
|
||||
lardon3d_image_catalog_format_size(1536, text, sizeof(text));
|
||||
CHECK(strcmp(text, "1,5 Kio") == 0);
|
||||
lardon3d_image_catalog_format_size(2 * 1024 * 1024, text, sizeof(text));
|
||||
CHECK(strcmp(text, "2,0 Mio") == 0);
|
||||
lardon3d_image_catalog_format_size(
|
||||
UINT64_C(3) * 1024 * 1024 * 1024,
|
||||
text,
|
||||
sizeof(text)
|
||||
);
|
||||
CHECK(strcmp(text, "3,0 Gio") == 0);
|
||||
lardon3d_image_catalog_format_size(0, NULL, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
run_test(void)
|
||||
{
|
||||
char base[] = "/tmp/lardon3d-catalog-test.XXXXXX";
|
||||
CHECK(mkdtemp(base));
|
||||
char project[PATH_MAX];
|
||||
char images[PATH_MAX];
|
||||
char originals[PATH_MAX];
|
||||
char manifest[PATH_MAX];
|
||||
CHECK(join_path(project, base, "project"));
|
||||
CHECK(join_path(images, project, "images"));
|
||||
CHECK(join_path(originals, images, "originals"));
|
||||
CHECK(join_path(manifest, images, "manifest.tsv"));
|
||||
CHECK(mkdir(project, 0755) == 0);
|
||||
CHECK(mkdir(images, 0755) == 0);
|
||||
CHECK(mkdir(originals, 0755) == 0);
|
||||
|
||||
Lardon3DAppState state;
|
||||
lardon3d_app_state_init(&state);
|
||||
state.project_loaded = true;
|
||||
CHECK(snprintf(
|
||||
state.project_path,
|
||||
sizeof(state.project_path),
|
||||
"%s",
|
||||
project
|
||||
) > 0);
|
||||
|
||||
char error[256];
|
||||
Lardon3DImageCatalog *catalog = lardon3d_image_catalog_load(
|
||||
&state,
|
||||
error,
|
||||
sizeof(error)
|
||||
);
|
||||
CHECK(catalog);
|
||||
CHECK(lardon3d_image_catalog_count(catalog) == 0);
|
||||
CHECK(strcmp(error, "Aucune image importée.") == 0);
|
||||
CHECK(lardon3d_image_catalog_get(catalog, 0) == NULL);
|
||||
lardon3d_image_catalog_destroy(catalog);
|
||||
lardon3d_image_catalog_destroy(NULL);
|
||||
|
||||
char first[PATH_MAX];
|
||||
char utf8[PATH_MAX];
|
||||
char link_path[PATH_MAX];
|
||||
CHECK(join_path(first, originals, "première image.jpg"));
|
||||
CHECK(join_path(utf8, originals, "été.png"));
|
||||
CHECK(join_path(link_path, originals, "lien.jpg"));
|
||||
CHECK(create_file(first, "abc"));
|
||||
CHECK(create_file(utf8, "12345"));
|
||||
|
||||
const char valid_manifest[] =
|
||||
"filename\tsize_bytes\tsource_path\n"
|
||||
"première image.jpg\t3\t/tmp/source avec espaces/première image.jpg\n"
|
||||
"été.png\t5\t/tmp/source/été.png\n";
|
||||
CHECK(replace_manifest(manifest, valid_manifest));
|
||||
catalog = lardon3d_image_catalog_load(&state, error, sizeof(error));
|
||||
CHECK(catalog);
|
||||
CHECK(error[0] == '\0');
|
||||
CHECK(lardon3d_image_catalog_count(catalog) == 2);
|
||||
CHECK(lardon3d_image_catalog_total_size(catalog) == 8);
|
||||
const Lardon3DImageEntry *entry = lardon3d_image_catalog_get(catalog, 0);
|
||||
CHECK(entry);
|
||||
CHECK(strcmp(entry->filename, "première image.jpg") == 0);
|
||||
CHECK(strcmp(
|
||||
entry->source_path,
|
||||
"/tmp/source avec espaces/première image.jpg"
|
||||
) == 0);
|
||||
CHECK(entry->size_bytes == 3);
|
||||
entry = lardon3d_image_catalog_get(catalog, 1);
|
||||
CHECK(entry && strcmp(entry->filename, "été.png") == 0);
|
||||
CHECK(lardon3d_image_catalog_get(catalog, 2) == NULL);
|
||||
lardon3d_image_catalog_destroy(catalog);
|
||||
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"bad\theader\npremière image.jpg\t3\t/tmp/source\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\npremière image.jpg\t3\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\npremière image.jpg\t3\t/a\textra\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\npremière image.jpg\t12x\t/a\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\n"
|
||||
"première image.jpg\t18446744073709551616\t/a\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\n\t3\t/a\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\ndir/image.jpg\t3\t/a\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\ndir\\image.jpg\t3\t/a\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\npremière image.jpg\t3\t\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\n"
|
||||
"première image.jpg\t3\t/a\npremière image.jpg\t3\t/b\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\nabsente.jpg\t1\t/a\n"
|
||||
));
|
||||
CHECK(symlink(first, link_path) == 0);
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\nlien.jpg\t3\t/a\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\npremière image.jpg\t4\t/a\n"
|
||||
));
|
||||
CHECK(expect_invalid(
|
||||
&state,
|
||||
manifest,
|
||||
"filename\tsize_bytes\tsource_path\npremière image.jpg\t3\t/a"
|
||||
));
|
||||
|
||||
CHECK(replace_manifest(manifest, valid_manifest));
|
||||
catalog = lardon3d_image_catalog_load(&state, error, sizeof(error));
|
||||
CHECK(catalog && lardon3d_image_catalog_count(catalog) == 2);
|
||||
lardon3d_image_catalog_destroy(catalog);
|
||||
CHECK(test_sizes());
|
||||
CHECK(remove_tree(base));
|
||||
return true;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
Loading…
Reference in a new issue