diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-22 02:02:27 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-22 02:02:27 +0000 |
| commit | 9efee50d405b8ca990d30114fb6ec6a57e2e92f1 (patch) | |
| tree | 3b29a1fb9e27b0a1e778f75555b05e9980f5b2a4 | |
| parent | 660d618f8b0df318d636cd780e547890d0248c0c (diff) | |
feat(bundle): open independent native windows with CtrlCmd+Shift+N
macOS only activates a running app, so a second document needs another
window in the same process. Each window label gets its own Runner, which
is stopped when its window is destroyed or the app exits, and the
capability now covers every window.
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/capabilities/default.json | 2 | ||||
| -rw-r--r-- | bundle/src-tauri/src/main.rs | 79 | ||||
| -rw-r--r-- | bundle/src/main.js | 2 |
5 files changed, 76 insertions, 15 deletions
diff --git a/bundle/AGENTS/DEVELOPMENT.md b/bundle/AGENTS/DEVELOPMENT.md index b535dd0..52d13ba 100644 --- a/bundle/AGENTS/DEVELOPMENT.md +++ b/bundle/AGENTS/DEVELOPMENT.md @@ -26,10 +26,10 @@ 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. A watch signal kills and reaps the process on Stop or application 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. +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 application 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. -Keyboard shortcuts are Monaco `KeyMod.CtrlCmd` keybindings: the editor registers Find and Toggle Line Comment, and the document handler matches New, Open, Save and Find with `StandardKeyboardEvent.equals`. Never test `ctrlKey`/`metaKey` directly; Monaco resolves CtrlCmd to Cmd on macOS and Ctrl elsewhere. +Keyboard shortcuts are Monaco `KeyMod.CtrlCmd` keybindings: the editor registers Find and Toggle Line Comment, and the document handler matches New, New Window (Shift), Open, Save and Find with `StandardKeyboardEvent.equals`. Never test `ctrlKey`/`metaKey` directly; Monaco resolves CtrlCmd to Cmd on macOS and Ctrl elsewhere. -Use brief English UI labels and messages. Keep the first version to one document and prefilled stdin. 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. +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 b9df146..bf696f7 100644 --- a/bundle/README.md +++ b/bundle/README.md @@ -2,7 +2,7 @@ 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. -Shortcuts use Cmd on macOS and Ctrl on Windows: **N** new, **O** open, **S** save, **F** find, **/** toggle line comment. +Shortcuts use Cmd on macOS and Ctrl on Windows: **N** new, **Shift+N** new window, **O** open, **S** save, **F** find, **/** toggle line comment. 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/capabilities/default.json b/bundle/src-tauri/capabilities/default.json index 4d2f698..a9c7253 100644 --- a/bundle/src-tauri/capabilities/default.json +++ b/bundle/src-tauri/capabilities/default.json @@ -1,6 +1,6 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main", - "windows": ["main"], + "windows": ["*"], "permissions": ["core:default", "core:window:allow-set-title", "core:window:allow-close", "dialog:default"] } diff --git a/bundle/src-tauri/src/main.rs b/bundle/src-tauri/src/main.rs index 07f25b3..80b28c5 100644 --- a/bundle/src-tauri/src/main.rs +++ b/bundle/src-tauri/src/main.rs @@ -4,7 +4,29 @@ mod compiler; mod runner; use compiler::Compiler; use runner::{OutputEvent, Runner}; -use tauri::{ipc::Channel, Manager, State}; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicUsize, Ordering}, + Mutex, + }, +}; +use tauri::{ipc::Channel, AppHandle, Manager, State, WebviewWindow, WebviewWindowBuilder}; + +// Each window runs its own program, keyed by window label. +#[derive(Default)] +struct Runners(Mutex<HashMap<String, Runner>>); + +impl Runners { + fn get(&self, label: &str) -> Runner { + self.0 + .lock() + .unwrap() + .entry(label.into()) + .or_default() + .clone() + } +} #[tauri::command] fn read_file(path: String) -> Result<String, String> { @@ -21,11 +43,13 @@ async fn run_file( path: String, input: String, output: Channel<OutputEvent>, - runner: State<'_, Runner>, + window: WebviewWindow, + runners: State<'_, Runners>, compiler: State<'_, Compiler>, ) -> Result<(), String> { let executable = compiler.inner().0.clone()?; - let result = runner + let result = runners + .get(window.label()) .run(executable, path.into(), input, |stream, bytes| { let _ = output.send(OutputEvent::Data { stream: stream.into(), @@ -42,25 +66,60 @@ async fn run_file( } #[tauri::command] -async fn stop_run(runner: State<'_, Runner>) -> Result<(), String> { - runner.stop().await; +async fn stop_run(window: WebviewWindow, runners: State<'_, Runners>) -> Result<(), String> { + runners.get(window.label()).stop().await; Ok(()) } +// Async because creating a window from a synchronous command deadlocks on Windows. +#[tauri::command] +async fn new_window(app: AppHandle) -> Result<(), String> { + static NEXT: AtomicUsize = AtomicUsize::new(1); + let mut config = app.config().app.windows[0].clone(); + config.label = format!("window-{}", NEXT.fetch_add(1, Ordering::Relaxed)); + WebviewWindowBuilder::from_config(&app, &config) + .and_then(|builder| builder.build()) + .map(drop) + .map_err(|error| error.to_string()) +} + fn main() { let compiler = Compiler::load(); tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) - .manage(Runner::default()) + .manage(Runners::default()) .manage(compiler) .invoke_handler(tauri::generate_handler![ - read_file, write_file, run_file, stop_run + read_file, write_file, run_file, stop_run, new_window ]) .build(tauri::generate_context!()) .expect("Could not start CPSC Bundle") - .run(|app, event| { - if matches!(event, tauri::RunEvent::ExitRequested { .. }) { - tauri::async_runtime::block_on(app.state::<Runner>().stop()); + .run(|app, event| match event { + tauri::RunEvent::ExitRequested { .. } => { + let runners: Vec<Runner> = app + .state::<Runners>() + .0 + .lock() + .unwrap() + .values() + .cloned() + .collect(); + tauri::async_runtime::block_on(async { + for runner in runners { + runner.stop().await; + } + }); + } + tauri::RunEvent::WindowEvent { + label, + event: tauri::WindowEvent::Destroyed, + .. + } => { + let runner = app.state::<Runners>().0.lock().unwrap().remove(&label); + if let Some(runner) = runner { + tauri::async_runtime::block_on(runner.stop()); + } } + _ => {} }); } diff --git a/bundle/src/main.js b/bundle/src/main.js index 991ef06..4d06998 100644 --- a/bundle/src/main.js +++ b/bundle/src/main.js @@ -139,6 +139,8 @@ async function action(callback) { // CtrlCmd is Cmd on macOS and Ctrl elsewhere, matching Monaco's own keybindings. const shortcuts = [ [KeyMod.CtrlCmd | KeyCode.KeyN, () => action(newFile)], + // Each window is an independent native OS window, even while this one runs. + [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyN, () => invoke('new_window').catch(report)], [KeyMod.CtrlCmd | KeyCode.KeyO, () => action(openFile)], [KeyMod.CtrlCmd | KeyCode.KeyS, () => action(saveFile)], [KeyMod.CtrlCmd | KeyCode.KeyF, () => { editor.focus(); editor.getAction('actions.find').run(); }] |
