mcap/src/app/library_folders.jai
Musa Mahmood 3e6e733a9d [0.0.13] Segmentation phase 4: per-folder opt-in, the segmentation scan, -segment
- .library-folders in the library (src/app/library_folders.jai): each library folder's options,
  starting with segment: true. Every folder starts off; unknown keys are kept when it is rewritten.
- The Segmentation window (Segments, above the grid): each folder's Segment checkbox, how many of
  its images are segmented, and Erase per folder; a button to segment the folders set to segment.
- The segmentation scan (src/app/segment_ui.jai) runs on the OCR thread as its own task, with the
  OCR scan's progress, pause and cancel: images only (never videos), not yet segmented, in folders
  set to segment. The engine loads with the first scan (2.4 GB of GPU memory). Each image's segs and
  mask are stored in one rewrite and its catalog entry loaded again.
- Importing into a folder set to segment segments the new images; the Import window's checkbox is
  the folder's setting.
- -segment segments and exits; -erase segments, and -erase all now includes segments.
- The catalog reads each capture's segs (class list hash, segment count). The exit overlay names the
  segmentation scan and model. segment_mcap_chunks moved into modules/Segment, shared with
  segment-probe. The quoting helpers of Capture_File's text formats are public.
- Tests: .library-folders read and written back (quoted names, unknown keys, versions).
2026-10-10 04:13:03 +01:00

147 lines
5.9 KiB
Plaintext

// Options for each library folder (a top-level folder of the library, one per place imported from),
// kept in .library-folders in the library itself, so they go where the library goes:
//
// @version 1
//
// [folder "Photos 2014"]
// segment: true
//
// segment: segmentation scans (app/segment_ui.jai) read the images in it. A folder not listed, or an
// option not given, is off. Keys this build does not know are kept as they are when the file is
// written again, so a later build's options survive an earlier one. Main thread only.
LIBRARY_FOLDERS_NAME :: ".library-folders";
Library_Folder :: struct {
name: string; // as viewer captures name it: "SnagitLibraryWork"; "" for the library's top
segment: bool;
other_lines: [..] string; // key: value lines this build does not know, as they were
}
library_folders: struct {
folders: [..] Library_Folder; // heap; in the file's order, new ones last
loaded: bool;
}
library_folders_path :: () -> string {
return tprint("%/%", viewer_folder(), LIBRARY_FOLDERS_NAME);
}
// Reads .library-folders; a missing file is every option off. Mistakes are logged and skipped.
library_folders_load :: () {
path := library_folders_path();
text, ok := read_entire_file(path, log_errors = false,, temp);
library_folders_parse(ifx ok then text else "", path);
}
// Writes .library-folders beside itself and swaps it in.
library_folders_save :: () {
path := library_folders_path();
temp_path := tprint("%.tmp", path);
if !write_entire_file(temp_path, library_folders_text()) || !atomic_file_replace_or_just_rename_if_destination_file_does_not_exist(temp_path, path) {
log_error("[Library] Could not write %.", path);
}
}
// The options in text, a .library-folders file's (path is for messages), in place of those held.
library_folders_parse :: (text: string, path: string) {
push_allocator(context.default_allocator);
library_folders_clear();
library_folders.folders.allocator = context.default_allocator;
library_folders.loaded = true;
folder: *Library_Folder;
version_seen := false;
for raw, line_index: split(text, "\n",, temp) {
line := trim(trim_right(raw, "\r"));
if !line.count || line[0] == #char "#" continue;
if begins_with(line, "@version") {
version, version_ok := string_to_int(trim(advance(line, 8)));
if !version_ok || version != 1 { log_error("[Library] % is version %, which this build does not read; its options are off.", path, trim(advance(line, 8))); library_folders_clear(); return; }
version_seen = true;
continue;
}
if !version_seen { log_error("[Library] % does not start with @version 1; its options are off.", path); return; }
if begins_with(line, "[folder ") && ends_with(line, "]") {
name, rest, name_ok := mcap_read_quoted(trim(slice(line, 8, line.count - 9)));
if !name_ok || trim(rest).count { log_error("[Library] %:%: expected [folder \"NAME\"].", path, line_index + 1); folder = null; continue; }
folder = library_folder_find_or_add(name);
continue;
}
colon := find_index_from_left(line, #char ":");
if !folder || colon < 0 { log_error("[Library] %:%: expected key: value under a [folder \"NAME\"]. Ignored.", path, line_index + 1); continue; }
key := trim(slice(line, 0, colon));
value := trim(advance(line, colon + 1));
if key == "segment" && (value == "true" || value == "false") folder.segment = value == "true";
else array_add(*folder.other_lines, copy_string(line));
}
}
// The options held, as a .library-folders file. Folders with no option on and nothing kept are left
// out. Temp.
library_folders_text :: () -> string {
builder: String_Builder;
builder.allocator = temp;
append(*builder, "@version 1\n");
append(*builder, "# Options for each library folder, set in mcap's Segmentation window. See src/app/library_folders.jai.\n");
for library_folders.folders {
if !it.segment && !it.other_lines.count continue;
print_to_builder(*builder, "\n[folder %]\n", mcap_quote(it.name));
print_to_builder(*builder, "segment: %\n", ifx it.segment then "true" else "false");
for line: it.other_lines print_to_builder(*builder, "%\n", line);
}
return builder_to_string(*builder,, temp);
}
library_folder_segments :: (name: string) -> bool {
for library_folders.folders if it.name == name return it.segment;
return false;
}
library_folder_set_segment :: (name: string, on: bool) {
folder := library_folder_find_or_add(name);
if folder.segment == on return;
folder.segment = on;
library_folders_save();
log("[Library] Segmentation % for %.", ifx on then "on" else "off", ifx name then name else "the library's top");
}
// Folders the file has options for that the library no longer holds (renamed or deleted). Temp.
library_folders_gone :: () -> [..] string {
gone: [..] string;
gone.allocator = temp;
for library_folders.folders if (it.segment || it.other_lines.count) && !array_find(viewer.folders, it.name) array_add(*gone, it.name);
return gone;
}
library_folder_forget :: (name: string) {
for library_folders.folders {
if it.name != name continue;
free(it.name,, context.default_allocator);
for line: it.other_lines free(line,, context.default_allocator);
array_free(it.other_lines);
array_ordered_remove_by_index(*library_folders.folders, it_index);
break;
}
library_folders_save();
}
#scope_file
library_folder_find_or_add :: (name: string) -> *Library_Folder {
for * library_folders.folders if it.name == name return it;
folder := array_add(*library_folders.folders);
folder.* = .{name = copy_string(name,, context.default_allocator)};
folder.other_lines.allocator = context.default_allocator;
return folder;
}
library_folders_clear :: () {
for library_folders.folders {
free(it.name,, context.default_allocator);
for line: it.other_lines free(line,, context.default_allocator);
array_free(it.other_lines);
}
array_reset(*library_folders.folders);
}