From 853307697a1a759025a83bad777ca4e35ca3ea3b Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 2 Jun 2026 23:14:35 +0200 Subject: [PATCH] Plan new document creation in app core --- docs/modernization/build-inventory.md | 11 ++- docs/modernization/capability-map.md | 2 +- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 9 +- src/app_core/document_session.h | 42 +++++++++ src/app_dialogs.cpp | 34 +++++--- tests/CMakeLists.txt | 20 +++++ tests/app_core/document_session_tests.cpp | 73 ++++++++++++++++ ...t_pano_cli_plan_new_document_failure.cmake | 26 ++++++ tools/pano_cli/main.cpp | 86 +++++++++++++++++++ 10 files changed, 284 insertions(+), 21 deletions(-) create mode 100644 tests/cmake/expect_pano_cli_plan_new_document_failure.cmake diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index ad7e5b9..fdad67e 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -397,6 +397,10 @@ Known local toolchain state: - `pano_cli plan-open-route` exposes `pp_app_core` document-open action planning as JSON and is covered for clean project open, dirty project discard-prompt, and ABR import-prompt states. +- `pano_cli plan-new-document` exposes `pp_app_core` new-document target, + legacy resolution-index mapping, and overwrite-prompt planning as JSON and is + covered for save-now, existing-target overwrite, and invalid-resolution + states. - `pano_cli plan-document-file` exposes `pp_app_core` document-name validation, legacy `.ppi` path construction, and overwrite-prompt decisions as JSON and is covered for save-now and existing-target overwrite states. @@ -420,9 +424,10 @@ Known local toolchain state: directory/stem targets, picked-directory stems, MP4 suggested names, and invalid export naming inputs. - `pp_app_core_document_session_tests` covers clean and dirty app session, - document-open action planning, save-request, save-before-workflow, document - file target, overwrite, and save-version target decisions without requiring - a window, canvas, or message box. + document-open action planning, save-request, save-before-workflow, + new-document target/resolution/overwrite planning, document file target, + overwrite, and save-version target decisions without requiring a window, + canvas, or message box. - `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON` through the vcpkg preset; default and Android validation still use the retained vendored fallback tracked by DEBT-0012. diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 0de7f58..865fc1d 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -13,7 +13,7 @@ and validation command. | --- | --- | --- | --- | | PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture | | Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, open-action plan tests, CLI route/action smoke, app open smoke | -| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/overwrite/version-target decision tests, CLI session, document-file, and document-version smoke, app close/open/save/new/browse smoke | +| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/new-document resolution/overwrite/version-target decision tests, CLI session, new-document, document-file, and document-version smoke, app close/open/save/new/browse smoke | | Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior | | Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input | | Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke | diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index a95b283..badac29 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -22,7 +22,7 @@ agent or engineer to remove them without reconstructing context from chat. | --- | --- | --- | --- | --- | --- | --- | | DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets | | DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation | -| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new/save-as document file naming and overwrite decisions, save-version target decisions, export target naming/path decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-target`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export contracts, but document loading, brush import execution, saving, and export execution still reach legacy `Canvas::I`/UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | +| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export target naming/path decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-target`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export contracts, but document creation/loading, brush import execution, saving, and export execution still reach legacy `Canvas::I`/UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | | DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path | | DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated | | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 87aa263..3610a16 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -428,9 +428,12 @@ document-open action plan for clean project open, dirty project prompt, and brush-import prompt flows. `pano_cli simulate-app-session` exposes the pure `pp_app_core` session decisions used by project-open, app-close, save, save-as, and save-version flows, plus the save-before-continue workflow gate used by -new-document/open/browse dialogs. `pano_cli plan-document-file` exposes the -same app-core document-name validation, legacy `.ppi` path construction, and -overwrite prompt decision used by new-document and save-as dialogs. +new-document/open/browse dialogs. `pano_cli plan-new-document` exposes the +same app-core new-document target, legacy resolution-index mapping, and +overwrite decision used by the live new-document dialog, including invalid +resolution rejection. `pano_cli plan-document-file` exposes the same app-core +document-name validation, legacy `.ppi` path construction, and overwrite +prompt decision used by save-as dialogs. `pano_cli plan-document-version` exposes the save-version suffix parsing, candidate generation, collision skipping, and no-slot failure behavior used by the live save-version dialog. diff --git a/src/app_core/document_session.h b/src/app_core/document_session.h index 24a09d0..a9a2c89 100644 --- a/src/app_core/document_session.h +++ b/src/app_core/document_session.h @@ -3,6 +3,7 @@ #include "app_core/document_route.h" #include "foundation/result.h" +#include #include #include #include @@ -65,6 +66,12 @@ struct DocumentVersionTarget { std::string path; }; +struct NewDocumentPlan { + DocumentFileTarget target; + int resolution = 0; + DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now; +}; + [[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept { return has_unsaved_changes @@ -175,6 +182,41 @@ struct DocumentVersionTarget { : DocumentFileWriteDecision::save_now; } +[[nodiscard]] constexpr pp::foundation::Result document_resolution_from_index(int index) noexcept +{ + constexpr std::array resolutions{ 512, 1024, 1536, 2048, 4096, 8192 }; + if (index < 0 || static_cast(index) >= resolutions.size()) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("document resolution index is out of range")); + } + + return pp::foundation::Result::success(resolutions[static_cast(index)]); +} + +template +[[nodiscard]] pp::foundation::Result plan_new_document( + std::string_view work_directory, + std::string_view document_name, + int resolution_index, + ExistsPredicate&& exists) +{ + const auto resolution = document_resolution_from_index(resolution_index); + if (!resolution) { + return pp::foundation::Result::failure(resolution.status()); + } + + auto target = make_document_file_target(work_directory, document_name); + if (!target) { + return pp::foundation::Result::failure(target.status()); + } + + NewDocumentPlan plan; + plan.target = std::move(target.value()); + plan.resolution = resolution.value(); + plan.write_decision = plan_document_file_write(exists(plan.target.path)); + return pp::foundation::Result::success(std::move(plan)); +} + [[nodiscard]] inline bool has_legacy_two_character_version_suffix(std::string_view document_name) noexcept { const auto dot = document_name.rfind('.'); diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp index 398bbfe..a40c311 100644 --- a/src/app_dialogs.cpp +++ b/src/app_dialogs.cpp @@ -2,6 +2,7 @@ #include "app.h" #include "action.h" #include "app_core/document_export.h" +#include "app_core/document_session.h" #include "settings.h" #include "node_dialog_open.h" #include "node_dialog_browse.h" @@ -158,24 +159,32 @@ void App::dialog_newdoc() dialog->btn_ok->on_click = [this, dialog](Node*) { std::string name = dialog->input->m_text; - const auto target = pp::app::make_document_file_target(work_path, name); - if (!target) + const auto plan = pp::app::plan_new_document( + work_path, + name, + dialog->m_resolution->m_current_index, + [](const std::string& path) { + return Asset::exist(path); + }); + if (!plan) { - message_box("Warning", "You need to specify a name to file."); + const bool missing_name = + plan.status().code == pp::foundation::StatusCode::invalid_argument; + message_box( + "Warning", + missing_name ? "You need to specify a name to file." : plan.status().message); return; } - auto action = [this, dialog, target = target.value()] { - std::array resolutions{ 512, 1024, 1536, 2048, 4096, 8192 }; - int res = resolutions[dialog->m_resolution->m_current_index]; - doc_name = target.name; - doc_path = target.path; - doc_filename = target.name + ".ppi"; - doc_dir = target.directory; + auto action = [this, dialog, plan = plan.value()] { + doc_name = plan.target.name; + doc_path = plan.target.path; + doc_filename = plan.target.name + ".ppi"; + doc_dir = plan.target.directory; layers->clear(); canvas->m_canvas->m_layers.clear(); - canvas->m_canvas->resize(res, res); + canvas->m_canvas->resize(plan.resolution, plan.resolution); canvas->reset_camera(); ActionManager::clear(); @@ -189,8 +198,7 @@ void App::dialog_newdoc() App::I->hideKeyboard(); }; - const auto write_decision = pp::app::plan_document_file_write(Asset::exist(target.value().path)); - if (write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite) + if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite) { // ask confirm is file already exist auto msgbox = new NodeMessageBox(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 44a273f..bb2e040 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -411,6 +411,26 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-file\".*\"path\":\"D:/Paint/demo.ppi\".*\"exists\":true.*\"decision\":\"prompt-overwrite\"") + add_test(NAME pano_cli_plan_new_document_smoke + COMMAND pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3) + set_tests_properties(pano_cli_plan_new_document_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-new-document\".*\"path\":\"D:/Paint/demo.ppi\".*\"exists\":false.*\"resolution\":2048.*\"decision\":\"save-now\"") + + add_test(NAME pano_cli_plan_new_document_overwrite_smoke + COMMAND pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 1 --target-exists) + set_tests_properties(pano_cli_plan_new_document_overwrite_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-new-document\".*\"path\":\"D:/Paint/demo.ppi\".*\"exists\":true.*\"resolution\":1024.*\"decision\":\"prompt-overwrite\"") + + add_test(NAME pano_cli_plan_new_document_rejects_invalid_resolution + COMMAND "${CMAKE_COMMAND}" + -DPANO_CLI=$ + "-DEXPECTED_OUTPUT=document resolution index is out of range" + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/expect_pano_cli_plan_new_document_failure.cmake") + set_tests_properties(pano_cli_plan_new_document_rejects_invalid_resolution PROPERTIES + LABELS "app;integration;desktop-fast;fuzz") + add_test(NAME pano_cli_plan_document_version_first_smoke COMMAND pano_cli plan-document-version --directory D:/Paint --doc-name demo) set_tests_properties(pano_cli_plan_document_version_first_smoke PROPERTIES diff --git a/tests/app_core/document_session_tests.cpp b/tests/app_core/document_session_tests.cpp index f1cdb26..18bafeb 100644 --- a/tests/app_core/document_session_tests.cpp +++ b/tests/app_core/document_session_tests.cpp @@ -175,6 +175,72 @@ void document_file_write_prompts_only_for_existing_targets(pp::tests::Harness& h == pp::app::DocumentFileWriteDecision::prompt_overwrite); } +void document_resolution_index_maps_legacy_choices(pp::tests::Harness& harness) +{ + const auto first = pp::app::document_resolution_from_index(0); + const auto last = pp::app::document_resolution_from_index(5); + PP_EXPECT(harness, first); + PP_EXPECT(harness, first.value() == 512); + PP_EXPECT(harness, last); + PP_EXPECT(harness, last.value() == 8192); +} + +void document_resolution_index_rejects_out_of_range(pp::tests::Harness& harness) +{ + const auto negative = pp::app::document_resolution_from_index(-1); + const auto too_large = pp::app::document_resolution_from_index(6); + PP_EXPECT(harness, !negative); + PP_EXPECT(harness, negative.status().code == pp::foundation::StatusCode::out_of_range); + PP_EXPECT(harness, !too_large); + PP_EXPECT(harness, too_large.status().code == pp::foundation::StatusCode::out_of_range); +} + +void new_document_plan_builds_target_resolution_and_write_decision(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_new_document( + "D:/Paint", + "demo", + 2, + [](const std::string&) { return false; }); + PP_EXPECT(harness, plan); + PP_EXPECT(harness, plan.value().target.name == "demo"); + PP_EXPECT(harness, plan.value().target.path == "D:/Paint/demo.ppi"); + PP_EXPECT(harness, plan.value().resolution == 1536); + PP_EXPECT(harness, plan.value().write_decision == pp::app::DocumentFileWriteDecision::save_now); +} + +void new_document_plan_prompts_for_existing_target(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_new_document( + "D:/Paint", + "demo", + 1, + [](const std::string& path) { return path == "D:/Paint/demo.ppi"; }); + PP_EXPECT(harness, plan); + PP_EXPECT(harness, plan.value().resolution == 1024); + PP_EXPECT( + harness, + plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite); +} + +void new_document_plan_rejects_invalid_inputs(pp::tests::Harness& harness) +{ + const auto missing_name = pp::app::plan_new_document( + "D:/Paint", + "", + 0, + [](const std::string&) { return false; }); + const auto invalid_resolution = pp::app::plan_new_document( + "D:/Paint", + "demo", + 99, + [](const std::string&) { return false; }); + PP_EXPECT(harness, !missing_name); + PP_EXPECT(harness, missing_name.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(harness, !invalid_resolution); + PP_EXPECT(harness, invalid_resolution.status().code == pp::foundation::StatusCode::out_of_range); +} + void document_version_target_starts_at_first_version(pp::tests::Harness& harness) { const auto target = pp::app::find_next_document_version_target( @@ -254,6 +320,13 @@ int main() harness.run("document file target rejects empty name", document_file_target_rejects_empty_name); harness.run("document file target builds legacy ppi path", document_file_target_builds_legacy_ppi_path); harness.run("document file write prompts only for existing targets", document_file_write_prompts_only_for_existing_targets); + harness.run("document resolution index maps legacy choices", document_resolution_index_maps_legacy_choices); + harness.run("document resolution index rejects out of range", document_resolution_index_rejects_out_of_range); + harness.run( + "new document plan builds target resolution and write decision", + new_document_plan_builds_target_resolution_and_write_decision); + harness.run("new document plan prompts for existing target", new_document_plan_prompts_for_existing_target); + harness.run("new document plan rejects invalid inputs", new_document_plan_rejects_invalid_inputs); harness.run("document version target starts at first version", document_version_target_starts_at_first_version); harness.run("document version target increments existing suffix", document_version_target_increments_existing_suffix); harness.run("document version target skips existing paths", document_version_target_skips_existing_paths); diff --git a/tests/cmake/expect_pano_cli_plan_new_document_failure.cmake b/tests/cmake/expect_pano_cli_plan_new_document_failure.cmake new file mode 100644 index 0000000..6c69a64 --- /dev/null +++ b/tests/cmake/expect_pano_cli_plan_new_document_failure.cmake @@ -0,0 +1,26 @@ +if(NOT DEFINED PANO_CLI) + message(FATAL_ERROR "PANO_CLI is required") +endif() + +if(NOT DEFINED EXPECTED_OUTPUT) + message(FATAL_ERROR "EXPECTED_OUTPUT is required") +endif() + +execute_process( + COMMAND "${PANO_CLI}" plan-new-document + --work-dir D:/Paint + --name demo + --resolution-index 99 + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE error) + +if(result EQUAL 0) + message(FATAL_ERROR "pano_cli plan-new-document unexpectedly succeeded: ${output}${error}") +endif() + +string(FIND "${output}${error}" "${EXPECTED_OUTPUT}" found_at) +if(found_at EQUAL -1) + message(FATAL_ERROR + "pano_cli plan-new-document failure output did not contain expected text.\nExpected: ${EXPECTED_OUTPUT}\nOutput: ${output}${error}") +endif() diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 6dae76a..dc2759e 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -108,6 +108,13 @@ struct PlanDocumentFileArgs { bool target_exists = false; }; +struct PlanNewDocumentArgs { + std::string work_directory; + std::string name; + std::uint32_t resolution_index = 0; + bool target_exists = false; +}; + struct PlanDocumentVersionArgs { std::string directory; std::string document_name; @@ -399,6 +406,7 @@ void print_help() << " inspect-project --path FILE\n" << " classify-open --path FILE\n" << " plan-open-route --path FILE [--unsaved]\n" + << " plan-new-document --work-dir DIR --name NAME [--resolution-index N] [--target-exists]\n" << " plan-document-file --work-dir DIR --name NAME [--target-exists]\n" << " plan-document-version --directory DIR --doc-name NAME [--existing-path FILE]\n" << " plan-export-target --kind file|collection|stem|name --doc-name NAME [--work-dir DIR] [--directory DIR] [--extension EXT] [--suffix SUFFIX]\n" @@ -1355,6 +1363,80 @@ int plan_document_file(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_new_document_args( + int argc, + char** argv, + PlanNewDocumentArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--work-dir") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.work_directory = argv[++i]; + } else if (key == "--name") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + args.name = argv[++i]; + } else if (key == "--resolution-index") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto parsed = pp::foundation::parse_u32(argv[++i]); + if (!parsed) { + return parsed.status(); + } + args.resolution_index = parsed.value(); + } else if (key == "--target-exists") { + args.target_exists = true; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + if (args.work_directory.empty()) { + return pp::foundation::Status::invalid_argument("work directory must not be empty"); + } + + return pp::foundation::Status::success(); +} + +int plan_new_document(int argc, char** argv) +{ + PlanNewDocumentArgs args; + const auto status = parse_plan_new_document_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-new-document", status.message); + return 2; + } + + const auto plan = pp::app::plan_new_document( + args.work_directory, + args.name, + static_cast(args.resolution_index), + [&args](const std::string&) { + return args.target_exists; + }); + if (!plan) { + print_error("plan-new-document", plan.status().message); + return 2; + } + + std::cout << "{\"ok\":true,\"command\":\"plan-new-document\"" + << ",\"target\":{\"name\":\"" << json_escape(plan.value().target.name) + << "\",\"directory\":\"" << json_escape(plan.value().target.directory) + << "\",\"path\":\"" << json_escape(plan.value().target.path) + << "\",\"exists\":" << json_bool(args.target_exists) + << "},\"document\":{\"resolution\":" << plan.value().resolution + << ",\"resolutionIndex\":" << args.resolution_index + << "},\"decision\":\"" + << document_file_write_decision_name(plan.value().write_decision) + << "\"}\n"; + return 0; +} + pp::foundation::Status parse_plan_document_version_args( int argc, char** argv, @@ -3559,6 +3641,10 @@ int main(int argc, char** argv) return plan_document_file(argc, argv); } + if (command == "plan-new-document") { + return plan_new_document(argc, argv); + } + if (command == "plan-document-version") { return plan_document_version(argc, argv); }