diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-27 12:37:44 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-27 12:37:44 +0000 |
| commit | 76ec25df16629b17f7d2775f4d2a54db0d022428 (patch) | |
| tree | 6c1cdbb227c52598389c1aff0e59f0e4680982f9 | |
| parent | 30178981f487c225901f404040c027d4d1f49fea (diff) | |
fix(bundle): automatically save changed documents before closing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | bundle/AGENTS/DEVELOPMENT.md | 6 | ||||
| -rw-r--r-- | bundle/README.md | 2 | ||||
| -rw-r--r-- | bundle/src-tauri/src/main.rs | 2 | ||||
| -rw-r--r-- | bundle/src-tauri/src/menu.rs | 2 | ||||
| -rw-r--r-- | bundle/src/main.js | 2 |
5 files changed, 9 insertions, 5 deletions
diff --git a/bundle/AGENTS/DEVELOPMENT.md b/bundle/AGENTS/DEVELOPMENT.md index 456c376..d63bbca 100644 --- a/bundle/AGENTS/DEVELOPMENT.md +++ b/bundle/AGENTS/DEVELOPMENT.md @@ -26,11 +26,13 @@ Windows and macOS nightly jobs cache Rust dependencies from `src-tauri/target` a The language editing core (grammar, configuration, indentation, lexical masking and bracket matching) is shared in full. Platform editor API adapters, file dialogs and process management are separate. Keep the plugin free of Bundle runtime dependencies. -The Rust runner launches the packaged compiler directly, using the source directory as cwd. On Windows its absolute path is captured after extracting into the startup working directory; elsewhere it is the sibling `cpsc` executable. Tokio concurrently writes prefilled stdin and drains both output pipes; stdin is dropped after writing. Each window label has its own Runner, so windows run independently; a watch signal kills and reaps the process on Stop, when its window is destroyed, or on `RunEvent::Exit`, which macOS also reaches when it terminates without closing windows (Dock Quit). The output Channel sends raw byte chunks (one streaming TextDecoder per stream in the frontend), then a finished event after both pipes reach EOF. EMBEDDED execution disables the host CRT stdout buffer before JIT initialization; Windows JIT lookups prefer the same msvcrt runtime. Do not introduce a shell or pseudo-terminal. The runner passes `-fdiagnostics-color=always`; `src/ansi.js` renders SGR bold and foreground colors from stderr only, so program stdout stays verbatim. Colors use `--vscode-terminal-ansi*`, which fall back to VS Code's terminal dark defaults pinned in `vendor/vscode/terminal-colors.json` because 2026 Dark does not set them. +The Rust runner launches the packaged compiler directly, using the source directory as cwd. On Windows its absolute path is captured after extracting into the startup working directory; elsewhere it is the sibling `cpsc` executable. Tokio concurrently writes prefilled stdin and drains both output pipes; stdin is dropped after writing. Each window label has its own Runner, so windows run independently; a watch signal kills and reaps the process on Stop, when its window is destroyed, or on `RunEvent::Exit`. User-requested application exit (including Dock Quit) closes the windows through their frontend handlers before allowing the app to exit. The output Channel sends raw byte chunks (one streaming TextDecoder per stream in the frontend), then a finished event after both pipes reach EOF. EMBEDDED execution disables the host CRT stdout buffer before JIT initialization; Windows JIT lookups prefer the same msvcrt runtime. Do not introduce a shell or pseudo-terminal. The runner passes `-fdiagnostics-color=always`; `src/ansi.js` renders SGR bold and foreground colors from stderr only, so program stdout stays verbatim. Colors use `--vscode-terminal-ansi*`, which fall back to VS Code's terminal dark defaults pinned in `vendor/vscode/terminal-colors.json` because 2026 Dark does not set them. After building the compiler, run `cargo test --release --manifest-path src-tauri/Cargo.toml`. The two native tests exercise real compilation, prefilled input, EOF, Unicode, diagnostics, output before process exit, stopping, single-run exclusion and restart, including source paths with spaces. CI runs them on all three native platforms. -The native menu is defined once in `src-tauri/src/menu.rs` with Tauri's menu API; macOS shows it in the system menu bar, Windows and Linux at the top of each window. Only the macOS application menu is platform-specific. Edit keeps the predefined clipboard and undo items, which macOS needs in the webview. macOS Quit (Cmd+Q) is a custom item that closes every window, so each runs its unsaved-changes prompt and the app exits after the last; the predefined Quit terminates without them. Windows and Linux close windows instead. Custom item ids name commands in `src/main.js`; a click emits a `menu` event with the id to the focused window. Run/Stop (F5) toggles like the Run button; its label is fixed because all windows share the menu. Keyboard shortcuts are Monaco `KeyMod.CtrlCmd` keybindings: the editor registers Find and Toggle Line Comment, and the document handler matches the menu's commands with `StandardKeyboardEvent.equals`, so a shortcut runs once whether the webview or the native menu receives the key first. Keep menu accelerators and handler bindings in sync. Never test `ctrlKey`/`metaKey` directly; Monaco resolves CtrlCmd to Cmd on macOS and Ctrl elsewhere. +The native menu is defined once in `src-tauri/src/menu.rs` with Tauri's menu API; macOS shows it in the system menu bar, Windows and Linux at the top of each window. Only the macOS application menu is platform-specific. Edit keeps the predefined clipboard and undo items, which macOS needs in the webview. macOS Quit (Cmd+Q) is a custom item that closes every window, so each saves changes and the app exits after the last. Windows and Linux close windows instead. Custom item ids name commands in `src/main.js`; a click emits a `menu` event with the id to the focused window. Run/Stop (F5) toggles like the Run button; its label is fixed because all windows share the menu. Keyboard shortcuts are Monaco `KeyMod.CtrlCmd` keybindings: the editor registers Find and Toggle Line Comment, and the document handler matches the menu's commands with `StandardKeyboardEvent.equals`, so a shortcut runs once whether the webview or the native menu receives the key first. Keep menu accelerators and handler bindings in sync. Never test `ctrlKey`/`metaKey` directly; Monaco resolves CtrlCmd to Cmd on macOS and Ctrl elsewhere. + +Run saves the document before launching the compiler. Close stops any running program, waits for the current file action, then saves dirty text before calling `window.destroy()`; capabilities must include `core:window:allow-destroy`. The close guard prevents duplicate requests and new file actions during this sequence and resets after cancellation or failure. A new document asks for a save location; canceling or write failure prevents running or closing. An unchanged blank window closes immediately. New/Open retain their Save/Discard/Cancel prompt. Use brief English UI labels and messages. Keep one document per window and prefilled stdin. More documents use native OS windows created by the `new_window` command from the configured window; do not add tabs or in-page switching. Before feature commits, run affected plugin tests, build the frontend and check native compilation as appropriate. Do not add a separate UI test framework for routine changes. diff --git a/bundle/README.md b/bundle/README.md index f7224e9..6147fc0 100644 --- a/bundle/README.md +++ b/bundle/README.md @@ -2,6 +2,8 @@ Open a `.cps` file, enter its input in **Input**, and click **Run**. The file is saved before running. Program output and errors appear in **Output**, with compiler errors colored. Click **Stop** to end a running program. +Closing a window or quitting the app automatically saves changes. New files ask for a save location before running or closing; canceling or a save failure keeps the window open and prevents running. An unchanged blank window closes without saving. Closing a running window stops its program first. + Shortcuts use Cmd on macOS and Ctrl on Windows: **N** new, **Shift+N** new window, **O** open, **S** save, **F** find, **/** toggle line comment. **F5** alone runs or stops the program. The menu bar lists these commands with their shortcuts: at the top of the screen on macOS, at the top of each window on Windows. Each window has its own file, Input and Run. Nightly provides a standalone Windows x64 `.exe` and a macOS Apple Silicon `.tar.xz` containing the `.app`. Windows extracts the included `cpsc.exe` into the current working directory on startup; macOS keeps it inside the `.app`. Windows uses the system WebView2 runtime. diff --git a/bundle/src-tauri/src/main.rs b/bundle/src-tauri/src/main.rs index 7dbccd9..c9babb4 100644 --- a/bundle/src-tauri/src/main.rs +++ b/bundle/src-tauri/src/main.rs @@ -102,7 +102,7 @@ fn main() { .on_menu_event(|app, event| { let mut windows = app.webview_windows().into_values(); if event.id() == "quit" { - // Each window runs its unsaved-changes prompt; the app exits after the last. + // Each window saves changes before closing; the app exits after the last. windows.for_each(|window| drop(window.close())); } else if let Some(window) = windows.find(|window| window.is_focused().unwrap_or(false)) { diff --git a/bundle/src-tauri/src/menu.rs b/bundle/src-tauri/src/menu.rs index dad3779..6d8507d 100644 --- a/bundle/src-tauri/src/menu.rs +++ b/bundle/src-tauri/src/menu.rs @@ -40,7 +40,7 @@ pub fn build<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> { .build()?; let menu = MenuBuilder::new(app); // macOS shows the first submenu as the application menu. The predefined Quit terminates - // without closing windows, skipping their unsaved-changes prompts, so "quit" closes them. + // without closing windows, so "quit" closes them to save changes first. #[cfg(target_os = "macos")] let menu = menu.item( &SubmenuBuilder::new(app, "CPSC Bundle") diff --git a/bundle/src/main.js b/bundle/src/main.js index 6734bc4..971816e 100644 --- a/bundle/src/main.js +++ b/bundle/src/main.js @@ -190,7 +190,7 @@ try { if (running) await invoke('stop_run'); // Finish any pending save/open or stopped run before closing the document. await actionDone; - if (!await canReplace()) return; + if (dirty() && !await saveFile()) return; await window.destroy(); } catch (error) { report(error); } finally { closing = false; |
