feat: add persistent project storage

This commit is contained in:
fy59 2026-08-06 17:41:42 +02:00
parent 59efdddef9
commit 2cc632c75e
9 changed files with 800 additions and 61 deletions

View file

@ -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. 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.

View file

@ -2,6 +2,7 @@
#define LARDON3D_APP_STATE_H #define LARDON3D_APP_STATE_H
#include <stdbool.h> #include <stdbool.h>
#include <limits.h>
typedef enum { typedef enum {
LARDON3D_SCREEN_HOME = 0, LARDON3D_SCREEN_HOME = 0,
@ -16,6 +17,7 @@ typedef struct {
bool running; bool running;
bool project_loaded; bool project_loaded;
char project_name[128]; char project_name[128];
char project_path[PATH_MAX];
char status_message[256]; char status_message[256];
} Lardon3DAppState; } Lardon3DAppState;

View file

@ -6,6 +6,7 @@
void lardon3d_layout_draw( void lardon3d_layout_draw(
const Lardon3DAppState *state, const Lardon3DAppState *state,
const char *project_name_input, const char *project_name_input,
const char *project_input_label,
int rows, int rows,
int cols int cols
); );

View file

@ -5,11 +5,16 @@
#include <lardon3d/app_state.h> #include <lardon3d/app_state.h>
bool lardon3d_project_set_name( bool lardon3d_project_create(
Lardon3DAppState *state, Lardon3DAppState *state,
const char *name const char *name
); );
bool lardon3d_project_open(
Lardon3DAppState *state,
const char *directory_name
);
void lardon3d_project_close(Lardon3DAppState *state); void lardon3d_project_close(Lardon3DAppState *state);
#endif #endif

226
scan3d/tri_photos.py Normal file
View file

@ -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())

View file

@ -12,6 +12,7 @@ lardon3d_app_state_init(Lardon3DAppState *state)
.running = true, .running = true,
.project_loaded = false, .project_loaded = false,
.project_name = "", .project_name = "",
.project_path = "",
.status_message = "Bienvenue dans Lardon3D", .status_message = "Bienvenue dans Lardon3D",
}; };
} }

View file

@ -64,7 +64,7 @@ screen_texts(
case LARDON3D_SCREEN_PROJECTS: case LARDON3D_SCREEN_PROJECTS:
*title = "Projets"; *title = "Projets";
*content = "Gestion des 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; break;
case LARDON3D_SCREEN_IMPORT: case LARDON3D_SCREEN_IMPORT:
*title = "Import"; *title = "Import";
@ -88,20 +88,25 @@ screen_texts(
} }
static void 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(6, 4, columns - 6, "N : Nouveau projet");
draw_text(7, 4, columns - 6, "C : Fermer le projet"); draw_text(7, 4, columns - 6, "O : Ouvrir un projet");
draw_text(8, 4, columns - 6, "ESC : Accueil"); draw_text(8, 4, columns - 6, "C : Fermer le projet");
draw_text(9, 4, columns - 6, "Q : Quitter"); draw_text(9, 4, columns - 6, "ESC : Accueil");
draw_text(10, 4, columns - 6, "Q : Quitter");
if (!project_name_input) { if (!project_name_input) {
return; return;
} }
draw_text(10, 4, columns - 6, "Nom du nouveau projet :"); draw_text(11, 4, columns - 6, project_input_label);
(void)mvaddch(11, 2, '['); (void)mvaddch(12, 2, '[');
(void)mvaddch(11, columns - 3, ']'); (void)mvaddch(12, columns - 3, ']');
int available = columns - 8; int available = columns - 8;
size_t length = strlen(project_name_input); 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; visible += length - (size_t)available;
length = (size_t)available; length = (size_t)available;
} }
draw_text(11, 4, available, visible); draw_text(12, 4, available, visible);
(void)move(11, 4 + (int)length); (void)move(12, 4 + (int)length);
} }
static void static void
draw_content( draw_content(
const Lardon3DAppState *state, const Lardon3DAppState *state,
const char *project_name_input, const char *project_name_input,
const char *project_input_label,
int rows, int rows,
int columns int columns
) )
@ -136,8 +142,15 @@ draw_content(
draw_text(1, (columns - title_length) / 2, title_length, title); draw_text(1, (columns - title_length) / 2, title_length, title);
draw_text(3, 2, columns - 4, "Projet"); draw_text(3, 2, columns - 4, "Projet");
draw_text(4, 4, columns - 6, project); 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) { if (state->screen == LARDON3D_SCREEN_PROJECTS) {
draw_project_screen(project_name_input, columns); draw_project_screen(
project_name_input,
project_input_label,
columns
);
} else { } else {
draw_text( draw_text(
(3 + journal_row) / 2, (3 + journal_row) / 2,
@ -155,6 +168,7 @@ void
lardon3d_layout_draw( lardon3d_layout_draw(
const Lardon3DAppState *state, const Lardon3DAppState *state,
const char *project_name_input, const char *project_name_input,
const char *project_input_label,
int rows, int rows,
int columns int columns
) )
@ -165,7 +179,13 @@ lardon3d_layout_draw(
draw_too_small(rows, columns); draw_too_small(rows, columns);
} else { } else {
draw_frame(rows, columns); 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(); (void)refresh();

View file

@ -1,57 +1,364 @@
#define _POSIX_C_SOURCE 200809L
#include <ctype.h> #include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <lardon3d/project.h> #include <lardon3d/project.h>
bool enum {
lardon3d_project_set_name(Lardon3DAppState *state, const char *name) MAX_CREATED_DIRECTORIES = 16,
{ INI_LINE_CAPACITY = 512,
if (!state) { };
return false;
}
if (!name) { typedef struct {
char paths[MAX_CREATED_DIRECTORIES][PATH_MAX];
size_t count;
} CreatedDirectories;
static void
set_status(Lardon3DAppState *state, const char *message)
{
(void)snprintf( (void)snprintf(
state->status_message, state->status_message,
sizeof(state->status_message), sizeof(state->status_message),
"Erreur : le nom du projet est vide." "%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; return false;
} }
const char *start = name; const char *start = input;
while (*start && isspace((unsigned char)*start)) { while (*start && isspace((unsigned char)*start)) {
++start; ++start;
} }
const char *end = name + strlen(name); const char *end = input + strlen(input);
while (end > start && isspace((unsigned char)end[-1])) { while (end > start && isspace((unsigned char)end[-1])) {
--end; --end;
} }
size_t length = (size_t)(end - start); size_t length = (size_t)(end - start);
if (length == 0) { if (length == 0) {
(void)snprintf( set_status(state, "Erreur : le nom du projet est vide.");
state->status_message, return false;
sizeof(state->status_message), }
"Erreur : le nom du projet est vide." 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; return false;
} }
if (length >= sizeof(state->project_name)) { for (const char *character = start; character < end; ++character) {
(void)snprintf( if (*character == '/' || *character == '\\'
state->status_message, || iscntrl((unsigned char)*character)) {
sizeof(state->status_message), set_status(state, "Erreur : nom de projet interdit.");
"Erreur : le nom du projet est trop long." 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; return false;
} }
(void)memcpy(state->project_name, start, length);
state->project_name[length] = '\0';
state->project_loaded = true; 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( (void)snprintf(
state->status_message, state->status_message,
sizeof(state->status_message), sizeof(state->status_message),
@ -61,6 +368,155 @@ lardon3d_project_set_name(Lardon3DAppState *state, const char *name)
return true; 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 void
lardon3d_project_close(Lardon3DAppState *state) lardon3d_project_close(Lardon3DAppState *state)
{ {
@ -69,19 +525,12 @@ lardon3d_project_close(Lardon3DAppState *state)
} }
if (!state->project_loaded) { if (!state->project_loaded) {
(void)snprintf( set_status(state, "Aucun projet à fermer.");
state->status_message,
sizeof(state->status_message),
"Aucun projet à fermer."
);
return; return;
} }
state->project_loaded = false; state->project_loaded = false;
state->project_name[0] = '\0'; state->project_name[0] = '\0';
(void)snprintf( state->project_path[0] = '\0';
state->status_message, set_status(state, "Projet fermé.");
sizeof(state->status_message),
"Projet fermé."
);
} }

View file

@ -15,7 +15,11 @@ enum {
}; };
typedef struct { typedef struct {
bool active; enum {
PROJECT_INPUT_NONE = 0,
PROJECT_INPUT_CREATE,
PROJECT_INPUT_OPEN,
} mode;
char text[PROJECT_INPUT_CAPACITY]; char text[PROJECT_INPUT_CAPACITY];
size_t length; size_t length;
} ProjectInput; } ProjectInput;
@ -27,10 +31,15 @@ redraw(const Lardon3DAppState *state, const ProjectInput *input)
int columns; int columns;
getmaxyx(stdscr, rows, columns); getmaxyx(stdscr, rows, columns);
const char *text = input->active ? input->text : NULL; const char *text = input->mode != PROJECT_INPUT_NONE ? input->text : NULL;
lardon3d_layout_draw(state, text, rows, columns); 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( (void)curs_set(
input->active && rows >= MINIMUM_ROWS && columns >= MINIMUM_COLUMNS input->mode != PROJECT_INPUT_NONE
&& rows >= MINIMUM_ROWS
&& columns >= MINIMUM_COLUMNS
? 1 ? 1
: 0 : 0
); );
@ -48,18 +57,26 @@ handle_project_input(
} }
if (key == 27) { 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( (void)snprintf(
state->status_message, state->status_message,
sizeof(state->status_message), sizeof(state->status_message),
"Création du projet annulée." "%s",
message
); );
return true; return true;
} }
if (key == '\n' || key == '\r' || key == KEY_ENTER) { if (key == '\n' || key == '\r' || key == KEY_ENTER) {
(void)lardon3d_project_set_name(state, input->text); if (input->mode == PROJECT_INPUT_OPEN) {
input->active = false; (void)lardon3d_project_open(state, input->text);
} else {
(void)lardon3d_project_create(state, input->text);
}
input->mode = PROJECT_INPUT_NONE;
return true; return true;
} }
@ -73,8 +90,12 @@ handle_project_input(
if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) { if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) {
if (input->length + 1 >= sizeof(input->text)) { if (input->length + 1 >= sizeof(input->text)) {
(void)lardon3d_project_set_name(state, input->text); if (input->mode == PROJECT_INPUT_OPEN) {
input->active = false; (void)lardon3d_project_open(state, input->text);
} else {
(void)lardon3d_project_create(state, input->text);
}
input->mode = PROJECT_INPUT_NONE;
return true; return true;
} }
@ -121,7 +142,18 @@ handle_normal_input(
case 'N': case 'N':
if (state->screen == LARDON3D_SCREEN_PROJECTS) { if (state->screen == LARDON3D_SCREEN_PROJECTS) {
*input = (ProjectInput) { *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 = "", .text = "",
.length = 0, .length = 0,
}; };
@ -172,7 +204,7 @@ lardon3d_tui_run(Lardon3DAppState *state)
return false; return false;
} }
bool should_redraw = input.active bool should_redraw = input.mode != PROJECT_INPUT_NONE
? handle_project_input(state, &input, key) ? handle_project_input(state, &input, key)
: handle_normal_input(state, &input, key); : handle_normal_input(state, &input, key);