diff --git a/README.md b/README.md index 14757c0..01035aa 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,6 @@ meson compile -C build -j8 ``` La TUI s'affiche directement dans le terminal courant. Appuyez sur `q` ou `Q` pour quitter proprement. + +Les projets sont enregistrés par défaut dans `~/Documents/Lardon/Projets3D`. +La variable `LARDON3D_PROJECTS_ROOT` permet de choisir un autre répertoire racine à l'exécution. diff --git a/include/lardon3d/app_state.h b/include/lardon3d/app_state.h index 219d3df..2934236 100644 --- a/include/lardon3d/app_state.h +++ b/include/lardon3d/app_state.h @@ -2,6 +2,7 @@ #define LARDON3D_APP_STATE_H #include +#include typedef enum { LARDON3D_SCREEN_HOME = 0, @@ -16,6 +17,7 @@ typedef struct { bool running; bool project_loaded; char project_name[128]; + char project_path[PATH_MAX]; char status_message[256]; } Lardon3DAppState; diff --git a/include/lardon3d/layout.h b/include/lardon3d/layout.h index d8b2a35..8accd12 100644 --- a/include/lardon3d/layout.h +++ b/include/lardon3d/layout.h @@ -6,6 +6,7 @@ void lardon3d_layout_draw( const Lardon3DAppState *state, const char *project_name_input, + const char *project_input_label, int rows, int cols ); diff --git a/include/lardon3d/project.h b/include/lardon3d/project.h index 6be226a..c06b549 100644 --- a/include/lardon3d/project.h +++ b/include/lardon3d/project.h @@ -5,11 +5,16 @@ #include -bool lardon3d_project_set_name( +bool lardon3d_project_create( Lardon3DAppState *state, const char *name ); +bool lardon3d_project_open( + Lardon3DAppState *state, + const char *directory_name +); + void lardon3d_project_close(Lardon3DAppState *state); #endif diff --git a/scan3d/tri_photos.py b/scan3d/tri_photos.py new file mode 100644 index 0000000..4552931 --- /dev/null +++ b/scan3d/tri_photos.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import csv +import re +import sys +from dataclasses import dataclass +from pathlib import Path + +import cv2 +import numpy as np +from PIL import Image + +EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp"} + + +@dataclass +class Result: + path: Path + category: str + sharpness: float + white: float + black: float + brightness: float + reason: str + + +def natural_key(path: Path) -> list[object]: + return [ + int(part) if part.isdigit() else part.lower() + for part in re.split(r"(\d+)", path.name) + ] + + +def load_gray(path: Path) -> np.ndarray: + with Image.open(path) as source: + source = source.convert("L") + gray = np.asarray(source) + + height, width = gray.shape + maximum_dimension = 1600 + + if max(width, height) > maximum_dimension: + scale = maximum_dimension / max(width, height) + gray = cv2.resize( + gray, + ( + max(1, round(width * scale)), + max(1, round(height * scale)), + ), + interpolation=cv2.INTER_AREA, + ) + + return gray + + +def analyze(path: Path) -> tuple[dict[str, float] | None, str]: + try: + gray = load_gray(path) + except Exception as error: + return None, f"illisible: {error}" + + return { + "sharpness": float(cv2.Laplacian(gray, cv2.CV_64F).var()), + "white": float(np.count_nonzero(gray >= 250) / gray.size * 100), + "black": float(np.count_nonzero(gray <= 5) / gray.size * 100), + "brightness": float(gray.mean()), + }, "" + + +def classify(path: Path, metrics: dict[str, float] | None, error: str) -> Result: + if metrics is None: + return Result(path, "mauvaises", 0.0, 0.0, 0.0, 0.0, error) + + sharpness = metrics["sharpness"] + white = metrics["white"] + black = metrics["black"] + brightness = metrics["brightness"] + + reasons: list[str] = [] + + if sharpness < 100: + reasons.append("très floue") + elif sharpness < 170: + reasons.append("floue possible") + + if white > 20: + reasons.append("très surexposée") + elif white > 8: + reasons.append("surexposition possible") + + if black > 35: + reasons.append("très sombre") + elif black > 15: + reasons.append("ombres bouchées possibles") + + if brightness > 225 or brightness < 25: + reasons.append("luminosité extrême") + + severe = ( + sharpness < 100 + or white > 20 + or black > 35 + or brightness > 225 + or brightness < 25 + ) + + if severe: + category = "mauvaises" + elif reasons: + category = "suspectes" + else: + category = "bonnes" + + return Result( + path, + category, + sharpness, + white, + black, + brightness, + "; ".join(reasons) or "ok", + ) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("images", type=Path) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + images_dir = args.images.expanduser().resolve() + output_dir = args.output.expanduser().resolve() + + excluded_roots = { + output_dir, + images_dir / "scan3d", + images_dir / "tri_resultat", + images_dir / "test_colmap", + images_dir / "colmap_complet", + } + + images: list[Path] = [] + for path in images_dir.rglob("*"): + if not path.is_file() or path.is_symlink(): + continue + if path.suffix.lower() not in EXTENSIONS: + continue + if any(root == path or root in path.parents for root in excluded_roots): + continue + images.append(path) + + images.sort(key=natural_key) + + if not images: + print("Aucune image trouvée.", file=sys.stderr) + return 1 + + print(f"{len(images)} images trouvées.") + + results: list[Result] = [] + for index, path in enumerate(images, 1): + metrics, error = analyze(path) + results.append(classify(path, metrics, error)) + print( + f"\rAnalyse {index}/{len(images)} : {path.name[:55]:55s}", + end="", + flush=True, + ) + print() + + output_dir.mkdir(parents=True, exist_ok=True) + for category in ("bonnes", "suspectes", "mauvaises"): + category_dir = output_dir / category + if category_dir.exists(): + for child in category_dir.iterdir(): + if child.is_symlink() or child.is_file(): + child.unlink() + category_dir.mkdir(parents=True, exist_ok=True) + + with (output_dir / "resultats.csv").open( + "w", encoding="utf-8", newline="" + ) as csv_file: + writer = csv.writer(csv_file) + writer.writerow( + [ + "fichier", + "categorie", + "netteté", + "blanc_pct", + "noir_pct", + "luminosité", + "raison", + ] + ) + + counters = {"bonnes": 0, "suspectes": 0, "mauvaises": 0} + for result in results: + counters[result.category] += 1 + link_name = f"{counters[result.category]:06d}_{result.path.name}" + link_path = output_dir / result.category / link_name + link_path.symlink_to(result.path.resolve()) + + writer.writerow( + [ + str(result.path), + result.category, + f"{result.sharpness:.2f}", + f"{result.white:.2f}", + f"{result.black:.2f}", + f"{result.brightness:.2f}", + result.reason, + ] + ) + + for category in ("bonnes", "suspectes", "mauvaises"): + count = sum(result.category == category for result in results) + print(f"{category}: {count}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/app_state.c b/src/app_state.c index 39d804c..e5466cb 100644 --- a/src/app_state.c +++ b/src/app_state.c @@ -12,6 +12,7 @@ lardon3d_app_state_init(Lardon3DAppState *state) .running = true, .project_loaded = false, .project_name = "", + .project_path = "", .status_message = "Bienvenue dans Lardon3D", }; } diff --git a/src/layout.c b/src/layout.c index 6e932f4..de2180c 100644 --- a/src/layout.c +++ b/src/layout.c @@ -64,7 +64,7 @@ screen_texts( case LARDON3D_SCREEN_PROJECTS: *title = "Projets"; *content = "Gestion des projets"; - *footer = "N Nouveau projet C Fermer le projet ESC Accueil Q Quit"; + *footer = "N Nouveau O Ouvrir C Fermer ESC Accueil Q Quit"; break; case LARDON3D_SCREEN_IMPORT: *title = "Import"; @@ -88,20 +88,25 @@ screen_texts( } static void -draw_project_screen(const char *project_name_input, int columns) +draw_project_screen( + const char *project_name_input, + const char *project_input_label, + int columns +) { draw_text(6, 4, columns - 6, "N : Nouveau projet"); - draw_text(7, 4, columns - 6, "C : Fermer le projet"); - draw_text(8, 4, columns - 6, "ESC : Accueil"); - draw_text(9, 4, columns - 6, "Q : Quitter"); + draw_text(7, 4, columns - 6, "O : Ouvrir un projet"); + draw_text(8, 4, columns - 6, "C : Fermer le projet"); + draw_text(9, 4, columns - 6, "ESC : Accueil"); + draw_text(10, 4, columns - 6, "Q : Quitter"); if (!project_name_input) { return; } - draw_text(10, 4, columns - 6, "Nom du nouveau projet :"); - (void)mvaddch(11, 2, '['); - (void)mvaddch(11, columns - 3, ']'); + draw_text(11, 4, columns - 6, project_input_label); + (void)mvaddch(12, 2, '['); + (void)mvaddch(12, columns - 3, ']'); int available = columns - 8; size_t length = strlen(project_name_input); @@ -110,14 +115,15 @@ draw_project_screen(const char *project_name_input, int columns) visible += length - (size_t)available; length = (size_t)available; } - draw_text(11, 4, available, visible); - (void)move(11, 4 + (int)length); + draw_text(12, 4, available, visible); + (void)move(12, 4 + (int)length); } static void draw_content( const Lardon3DAppState *state, const char *project_name_input, + const char *project_input_label, int rows, int columns ) @@ -136,8 +142,15 @@ draw_content( draw_text(1, (columns - title_length) / 2, title_length, title); draw_text(3, 2, columns - 4, "Projet"); draw_text(4, 4, columns - 6, project); + if (state->project_loaded) { + draw_text(5, 4, columns - 6, state->project_path); + } if (state->screen == LARDON3D_SCREEN_PROJECTS) { - draw_project_screen(project_name_input, columns); + draw_project_screen( + project_name_input, + project_input_label, + columns + ); } else { draw_text( (3 + journal_row) / 2, @@ -155,6 +168,7 @@ void lardon3d_layout_draw( const Lardon3DAppState *state, const char *project_name_input, + const char *project_input_label, int rows, int columns ) @@ -165,7 +179,13 @@ lardon3d_layout_draw( draw_too_small(rows, columns); } else { draw_frame(rows, columns); - draw_content(state, project_name_input, rows, columns); + draw_content( + state, + project_name_input, + project_input_label, + rows, + columns + ); } (void)refresh(); diff --git a/src/project.c b/src/project.c index a018dad..94c2bfd 100644 --- a/src/project.c +++ b/src/project.c @@ -1,57 +1,364 @@ +#define _POSIX_C_SOURCE 200809L + #include +#include +#include +#include #include +#include #include +#include +#include +#include #include -bool -lardon3d_project_set_name(Lardon3DAppState *state, const char *name) +enum { + MAX_CREATED_DIRECTORIES = 16, + INI_LINE_CAPACITY = 512, +}; + +typedef struct { + char paths[MAX_CREATED_DIRECTORIES][PATH_MAX]; + size_t count; +} CreatedDirectories; + +static void +set_status(Lardon3DAppState *state, const char *message) { - if (!state) { + (void)snprintf( + state->status_message, + sizeof(state->status_message), + "%s", + message + ); +} + +static bool +normalize_name( + Lardon3DAppState *state, + const char *input, + char *output, + size_t output_size +) +{ + if (!input) { + set_status(state, "Erreur : le nom du projet est vide."); return false; } - if (!name) { - (void)snprintf( - state->status_message, - sizeof(state->status_message), - "Erreur : le nom du projet est vide." - ); - return false; - } - - const char *start = name; + const char *start = input; while (*start && isspace((unsigned char)*start)) { ++start; } - const char *end = name + strlen(name); + const char *end = input + strlen(input); while (end > start && isspace((unsigned char)end[-1])) { --end; } size_t length = (size_t)(end - start); if (length == 0) { - (void)snprintf( - state->status_message, - sizeof(state->status_message), - "Erreur : le nom du projet est vide." - ); + set_status(state, "Erreur : le nom du projet est vide."); + return false; + } + if (length >= output_size) { + set_status(state, "Erreur : le nom du projet est trop long."); + return false; + } + if ((length == 1 && start[0] == '.') + || (length == 2 && start[0] == '.' && start[1] == '.')) { + set_status(state, "Erreur : nom de projet interdit."); return false; } - if (length >= sizeof(state->project_name)) { - (void)snprintf( - state->status_message, - sizeof(state->status_message), - "Erreur : le nom du projet est trop long." + for (const char *character = start; character < end; ++character) { + if (*character == '/' || *character == '\\' + || iscntrl((unsigned char)*character)) { + set_status(state, "Erreur : nom de projet interdit."); + return false; + } + } + + (void)memcpy(output, start, length); + output[length] = '\0'; + return true; +} + +static bool +copy_path(char *destination, size_t size, const char *source) +{ + int written = snprintf(destination, size, "%s", source); + return written >= 0 && (size_t)written < size; +} + +static bool +join_path( + char *destination, + size_t size, + const char *parent, + const char *child +) +{ + int written = snprintf(destination, size, "%s/%s", parent, child); + return written >= 0 && (size_t)written < size; +} + +static bool +resolve_projects_root(Lardon3DAppState *state, char root[PATH_MAX]) +{ + const char *configured = getenv("LARDON3D_PROJECTS_ROOT"); + if (configured && configured[0]) { + if (configured[0] != '/' || !copy_path(root, PATH_MAX, configured)) { + set_status(state, "Erreur : répertoire racine invalide."); + return false; + } + } else { + const char *home = getenv("HOME"); + if (!home || home[0] != '/') { + set_status(state, "Erreur : HOME est absent ou invalide."); + return false; + } + + int written = snprintf( + root, + PATH_MAX, + "%s/Documents/Lardon/Projets3D", + home ); + if (written < 0 || (size_t)written >= PATH_MAX) { + set_status(state, "Erreur : chemin racine trop long."); + return false; + } + } + + size_t length = strlen(root); + while (length > 1 && root[length - 1] == '/') { + root[--length] = '\0'; + } + return true; +} + +static void +cleanup_directories(CreatedDirectories *created) +{ + while (created->count > 0) { + --created->count; + (void)rmdir(created->paths[created->count]); + } +} + +static bool +ensure_directory( + Lardon3DAppState *state, + const char *path, + CreatedDirectories *created +) +{ + struct stat info; + if (lstat(path, &info) == 0) { + if (!S_ISDIR(info.st_mode)) { + set_status(state, "Erreur : un élément du chemin n'est pas un dossier."); + return false; + } + return true; + } + if (errno != ENOENT || created->count >= MAX_CREATED_DIRECTORIES) { + set_status(state, "Erreur : impossible de préparer le répertoire racine."); + return false; + } + if (mkdir(path, 0755) != 0) { + set_status(state, "Erreur : impossible de créer un dossier."); + return false; + } + if (!copy_path( + created->paths[created->count], + sizeof(created->paths[created->count]), + path + )) { + (void)rmdir(path); + set_status(state, "Erreur : chemin de dossier trop long."); + return false; + } + ++created->count; + return true; +} + +static bool +ensure_directory_tree( + Lardon3DAppState *state, + const char *path, + CreatedDirectories *created +) +{ + char partial[PATH_MAX]; + if (!copy_path(partial, sizeof(partial), path)) { + set_status(state, "Erreur : chemin racine trop long."); + return false; + } + + for (char *separator = partial + 1; *separator; ++separator) { + if (*separator != '/') { + continue; + } + *separator = '\0'; + bool success = ensure_directory(state, partial, created); + *separator = '/'; + if (!success) { + return false; + } + } + return ensure_directory(state, partial, created); +} + +static bool +write_project_ini( + Lardon3DAppState *state, + const char *project_path, + const char *project_name +) +{ + char temporary_path[PATH_MAX]; + char final_path[PATH_MAX]; + if (!join_path(final_path, sizeof(final_path), project_path, "project.ini") + || !join_path( + temporary_path, + sizeof(temporary_path), + project_path, + ".project.ini.tmp.XXXXXX" + )) { + set_status(state, "Erreur : chemin de project.ini trop long."); + return false; + } + + int descriptor = mkstemp(temporary_path); + if (descriptor < 0) { + set_status(state, "Erreur : impossible de créer project.ini."); + return false; + } + + FILE *file = fdopen(descriptor, "w"); + if (!file) { + (void)close(descriptor); + (void)unlink(temporary_path); + set_status(state, "Erreur : impossible d'écrire project.ini."); + return false; + } + + bool success = fprintf( + file, + "[project]\nname=%s\nversion=1\n", + project_name + ) >= 0; + if (success) { + success = fflush(file) == 0; + } + if (success) { + success = fsync(fileno(file)) == 0; + } + if (fclose(file) != 0) { + success = false; + } + if (success) { + success = rename(temporary_path, final_path) == 0; + } + if (!success) { + (void)unlink(temporary_path); + set_status(state, "Erreur : impossible d'écrire project.ini."); + } + return success; +} + +bool +lardon3d_project_create(Lardon3DAppState *state, const char *name) +{ + if (!state) { + return false; + } + + char normalized_name[sizeof(state->project_name)]; + char root[PATH_MAX]; + char project_path[PATH_MAX]; + if (!normalize_name( + state, + name, + normalized_name, + sizeof(normalized_name) + )) { + return false; + } + if (!resolve_projects_root(state, root)) { + return false; + } + if (!join_path( + project_path, + sizeof(project_path), + root, + normalized_name + )) { + set_status(state, "Erreur : chemin du projet trop long."); + return false; + } + + CreatedDirectories created = {0}; + if (!ensure_directory_tree(state, root, &created)) { + cleanup_directories(&created); + return false; + } + + struct stat info; + if (lstat(project_path, &info) == 0 || errno != ENOENT) { + cleanup_directories(&created); + set_status(state, "Erreur : ce projet existe déjà."); + return false; + } + if (!ensure_directory(state, project_path, &created)) { + cleanup_directories(&created); + return false; + } + + const char *subdirectories[] = { + "images", + "reconstruction", + "exports", + "logs", + }; + for (size_t index = 0; + index < sizeof(subdirectories) / sizeof(subdirectories[0]); + ++index) { + char path[PATH_MAX]; + if (!join_path( + path, + sizeof(path), + project_path, + subdirectories[index] + )) { + set_status(state, "Erreur : chemin de dossier trop long."); + cleanup_directories(&created); + return false; + } + if (!ensure_directory(state, path, &created)) { + cleanup_directories(&created); + return false; + } + } + + if (!write_project_ini(state, project_path, normalized_name)) { + cleanup_directories(&created); return false; } - (void)memcpy(state->project_name, start, length); - state->project_name[length] = '\0'; state->project_loaded = true; + (void)copy_path( + state->project_name, + sizeof(state->project_name), + normalized_name + ); + (void)copy_path( + state->project_path, + sizeof(state->project_path), + project_path + ); (void)snprintf( state->status_message, sizeof(state->status_message), @@ -61,6 +368,155 @@ lardon3d_project_set_name(Lardon3DAppState *state, const char *name) return true; } +static bool +read_project_ini( + Lardon3DAppState *state, + const char *path, + char project_name[128] +) +{ + int descriptor = open(path, O_RDONLY | O_NOFOLLOW); + if (descriptor < 0) { + set_status(state, "Erreur : project.ini est absent ou inaccessible."); + return false; + } + + struct stat info; + if (fstat(descriptor, &info) != 0 || !S_ISREG(info.st_mode)) { + (void)close(descriptor); + set_status(state, "Erreur : project.ini n'est pas un fichier régulier."); + return false; + } + + FILE *file = fdopen(descriptor, "r"); + if (!file) { + (void)close(descriptor); + set_status(state, "Erreur : impossible de lire project.ini."); + return false; + } + + bool in_project_section = false; + bool section_found = false; + bool name_found = false; + bool version_found = false; + bool valid = true; + char line[INI_LINE_CAPACITY]; + + while (valid && fgets(line, sizeof(line), file)) { + size_t length = strlen(line); + if (length > 0 && line[length - 1] == '\n') { + line[--length] = '\0'; + } else if (!feof(file)) { + valid = false; + break; + } + if (length > 0 && line[length - 1] == '\r') { + line[--length] = '\0'; + } + + if (strcmp(line, "[project]") == 0) { + in_project_section = true; + section_found = true; + } else if (line[0] == '[') { + in_project_section = false; + } else if (in_project_section && strncmp(line, "name=", 5) == 0) { + if (name_found || !normalize_name( + state, + line + 5, + project_name, + sizeof(state->project_name) + )) { + valid = false; + } + name_found = true; + } else if (in_project_section && strncmp(line, "version=", 8) == 0) { + if (version_found || strcmp(line, "version=1") != 0) { + valid = false; + } + version_found = true; + } + } + + if (ferror(file)) { + valid = false; + } + if (fclose(file) != 0) { + valid = false; + } + if (!valid || !section_found || !name_found || !version_found) { + set_status(state, "Erreur : project.ini invalide."); + return false; + } + return true; +} + +bool +lardon3d_project_open( + Lardon3DAppState *state, + const char *directory_name +) +{ + if (!state) { + return false; + } + + char normalized_directory[sizeof(state->project_name)]; + char root[PATH_MAX]; + char project_path[PATH_MAX]; + char ini_path[PATH_MAX]; + if (!normalize_name( + state, + directory_name, + normalized_directory, + sizeof(normalized_directory) + )) { + return false; + } + if (!resolve_projects_root(state, root)) { + return false; + } + if (!join_path( + project_path, + sizeof(project_path), + root, + normalized_directory + ) + || !join_path(ini_path, sizeof(ini_path), project_path, "project.ini")) { + set_status(state, "Erreur : chemin du projet trop long."); + return false; + } + + struct stat info; + if (lstat(project_path, &info) != 0 || !S_ISDIR(info.st_mode)) { + set_status(state, "Erreur : dossier projet absent ou invalide."); + return false; + } + + char project_name[sizeof(state->project_name)]; + if (!read_project_ini(state, ini_path, project_name)) { + return false; + } + + state->project_loaded = true; + (void)copy_path( + state->project_name, + sizeof(state->project_name), + project_name + ); + (void)copy_path( + state->project_path, + sizeof(state->project_path), + project_path + ); + (void)snprintf( + state->status_message, + sizeof(state->status_message), + "Projet ouvert : %s", + state->project_name + ); + return true; +} + void lardon3d_project_close(Lardon3DAppState *state) { @@ -69,19 +525,12 @@ lardon3d_project_close(Lardon3DAppState *state) } if (!state->project_loaded) { - (void)snprintf( - state->status_message, - sizeof(state->status_message), - "Aucun projet à fermer." - ); + set_status(state, "Aucun projet à fermer."); return; } state->project_loaded = false; state->project_name[0] = '\0'; - (void)snprintf( - state->status_message, - sizeof(state->status_message), - "Projet fermé." - ); + state->project_path[0] = '\0'; + set_status(state, "Projet fermé."); } diff --git a/src/tui.c b/src/tui.c index abf93a0..c045e26 100644 --- a/src/tui.c +++ b/src/tui.c @@ -15,7 +15,11 @@ enum { }; typedef struct { - bool active; + enum { + PROJECT_INPUT_NONE = 0, + PROJECT_INPUT_CREATE, + PROJECT_INPUT_OPEN, + } mode; char text[PROJECT_INPUT_CAPACITY]; size_t length; } ProjectInput; @@ -27,10 +31,15 @@ redraw(const Lardon3DAppState *state, const ProjectInput *input) int columns; getmaxyx(stdscr, rows, columns); - const char *text = input->active ? input->text : NULL; - lardon3d_layout_draw(state, text, rows, columns); + const char *text = input->mode != PROJECT_INPUT_NONE ? input->text : NULL; + const char *label = input->mode == PROJECT_INPUT_OPEN + ? "Nom du dossier projet :" + : "Nom du nouveau projet :"; + lardon3d_layout_draw(state, text, label, rows, columns); (void)curs_set( - input->active && rows >= MINIMUM_ROWS && columns >= MINIMUM_COLUMNS + input->mode != PROJECT_INPUT_NONE + && rows >= MINIMUM_ROWS + && columns >= MINIMUM_COLUMNS ? 1 : 0 ); @@ -48,18 +57,26 @@ handle_project_input( } if (key == 27) { - input->active = false; + const char *message = input->mode == PROJECT_INPUT_OPEN + ? "Ouverture du projet annulée." + : "Création du projet annulée."; + input->mode = PROJECT_INPUT_NONE; (void)snprintf( state->status_message, sizeof(state->status_message), - "Création du projet annulée." + "%s", + message ); return true; } if (key == '\n' || key == '\r' || key == KEY_ENTER) { - (void)lardon3d_project_set_name(state, input->text); - input->active = false; + if (input->mode == PROJECT_INPUT_OPEN) { + (void)lardon3d_project_open(state, input->text); + } else { + (void)lardon3d_project_create(state, input->text); + } + input->mode = PROJECT_INPUT_NONE; return true; } @@ -73,8 +90,12 @@ handle_project_input( if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) { if (input->length + 1 >= sizeof(input->text)) { - (void)lardon3d_project_set_name(state, input->text); - input->active = false; + if (input->mode == PROJECT_INPUT_OPEN) { + (void)lardon3d_project_open(state, input->text); + } else { + (void)lardon3d_project_create(state, input->text); + } + input->mode = PROJECT_INPUT_NONE; return true; } @@ -121,7 +142,18 @@ handle_normal_input( case 'N': if (state->screen == LARDON3D_SCREEN_PROJECTS) { *input = (ProjectInput) { - .active = true, + .mode = PROJECT_INPUT_CREATE, + .text = "", + .length = 0, + }; + return true; + } + return false; + case 'o': + case 'O': + if (state->screen == LARDON3D_SCREEN_PROJECTS) { + *input = (ProjectInput) { + .mode = PROJECT_INPUT_OPEN, .text = "", .length = 0, }; @@ -172,7 +204,7 @@ lardon3d_tui_run(Lardon3DAppState *state) return false; } - bool should_redraw = input.active + bool should_redraw = input.mode != PROJECT_INPUT_NONE ? handle_project_input(state, &input, key) : handle_normal_input(state, &input, key);