git.iko.soy/rowtype-yoga/purescript-tauri
on main help sign in
Add app lifecycle and native stdio controls
commita007050726af5085d37e5ad48b1b44987bab2b05 authorMark <mark.eibes@gmail.com> dateMon 5 Oct 2026, 00:39 (1 week ago) parentbbce120
8 files changed+104−5
native/bundled-process/build.rs+4−1 native/bundled-process/src/lib.rs+36−1 src/Tauri/App.js+5 src/Tauri/App.purs+21 src/Tauri/Process.js+6 src/Tauri/Process.purs+15−2 src/Tauri/Window.js+4 src/Tauri/Window.purs+13−1
native/bundled-process/build.rs+4−1
@@ -1,3 +1,6 @@
11fn main() {
2− tauri_plugin::Builder::new(&["run", "spawn", "write", "finish", "cancel"]).build();
2+ tauri_plugin::Builder::new(&[
3+ "run", "spawn", "write", "finish", "cancel", "stdout", "stderr",
4+ ])
5+ .build();
36}
native/bundled-process/src/lib.rs+36−1
@@ -483,6 +483,19 @@ fn raw_stream_bytes<'a>(request: &'a Request<'_>) -> Result<&'a [u8], String> {
483483 Ok(bytes)
484484}
485485
486+fn write_native_stream(
487+ mut stream: impl Write,
488+ text: String,
489+ stream_name: &str,
490+) -> Result<(), String> {
491+ stream
492+ .write_all(text.as_bytes())
493+ .map_err(|error| format!("Cannot write application {stream_name}: {error}"))?;
494+ stream
495+ .flush()
496+ .map_err(|error| format!("Cannot flush application {stream_name}: {error}"))
497+}
498+
486499fn take_streams(streams: &Streams, owner: Option<&str>) -> Vec<Arc<Stream>> {
487500 let mut active = streams.active.lock();
488501 let ids: Vec<String> = active
@@ -527,6 +540,26 @@ async fn run(
527540 .map_err(|error| format!("Bundled process worker failed: {error}"))?
528541}
529542
543+#[tauri::command]
544+async fn stdout(text: String) -> Result<(), String> {
545+ tauri::async_runtime::spawn_blocking(move || {
546+ let native_stdout = io::stdout();
547+ write_native_stream(native_stdout.lock(), text, "stdout")
548+ })
549+ .await
550+ .map_err(|error| format!("Bundled process worker failed: {error}"))?
551+}
552+
553+#[tauri::command]
554+async fn stderr(text: String) -> Result<(), String> {
555+ tauri::async_runtime::spawn_blocking(move || {
556+ let native_stderr = io::stderr();
557+ write_native_stream(native_stderr.lock(), text, "stderr")
558+ })
559+ .await
560+ .map_err(|error| format!("Bundled process worker failed: {error}"))?
561+}
562+
530563#[tauri::command]
531564fn spawn<R: Runtime>(
532565 process: State<'_, BundledProcess>,
@@ -602,7 +635,9 @@ async fn cancel<R: Runtime>(
602635pub fn init<R: Runtime>(executables: &[&str]) -> TauriPlugin<R> {
603636 let executables: Vec<String> = executables.iter().map(|name| (*name).to_owned()).collect();
604637 Builder::new("bundled-process")
605− .invoke_handler(tauri::generate_handler![run, spawn, write, finish, cancel])
638+ .invoke_handler(tauri::generate_handler![
639+ run, spawn, write, finish, cancel, stdout, stderr
640+ ])
606641 .setup(move |app, _| {
607642 for name in &executables {
608643 validate_basename(name)
src/Tauri/App.jsnew file+5
@@ -0,0 +1,5 @@
1+import { exit, setDockVisibility } from "@tauri-apps/api/app";
2+
3+export const exitImpl = code => () => exit(code);
4+
5+export const setDockVisibilityImpl = visible => () => setDockVisibility(visible);
src/Tauri/App.pursnew file+21
@@ -0,0 +1,21 @@
1+-- | Official Tauri app controls. `exit` requires @tauri-apps/api 2.12.0 or later,
2+-- | core:app:allow-exit, and core:app:allow-set-dock-visibility as applicable.
3+module Tauri.App (exit, setDockVisibility) where
4+
5+import Prelude (Unit)
6+
7+import Effect (Effect)
8+import Effect.Aff (Aff)
9+import Promise (Promise)
10+import Promise.Aff (toAffE)
11+
12+-- | Terminate the native application with the supplied process exit code.
13+exit :: Int -> Aff Unit
14+exit code = toAffE (exitImpl code)
15+
16+-- | Show or hide the Dock icon. This is supported by Tauri on macOS only.
17+setDockVisibility :: Boolean -> Aff Unit
18+setDockVisibility visible = toAffE (setDockVisibilityImpl visible)
19+
20+foreign import exitImpl :: Int -> Effect (Promise Unit)
21+foreign import setDockVisibilityImpl :: Boolean -> Effect (Promise Unit)
src/Tauri/Process.js+6
@@ -17,3 +17,9 @@ export const finishImpl = stream => () =>
1717
1818export const cancelImpl = stream => () =>
1919 invoke("plugin:bundled-process|cancel", { stream });
20+
21+export const stdoutImpl = text => () =>
22+ invoke("plugin:bundled-process|stdout", { text });
23+
24+export const stderrImpl = text => () =>
25+ invoke("plugin:bundled-process|stderr", { text });
src/Tauri/Process.purs+15−2
@@ -1,7 +1,8 @@
11-- | Run explicitly allowlisted executables beside the native application.
22-- | Register tauri-plugin-bundled-process and grant its command permissions only
3−-- | to the webview that owns the process stream.
4−module Tauri.Process (Result, Stream, run, spawn, write, finish, cancel) where
3+-- | to the webview that owns a process stream. `stdout` and `stderr` require
4+-- | bundled-process:allow-stdout and bundled-process:allow-stderr respectively.
5+module Tauri.Process (Result, Stream, run, spawn, write, finish, cancel, stdout, stderr) where
56
67import Data.ArrayBuffer.Types (ArrayBuffer)
78import Effect (Effect)
@@ -44,6 +45,16 @@ finish stream = toAffE (finishImpl stream)
4445cancel :: Stream -> Aff Unit
4546cancel stream = toAffE (cancelImpl stream)
4647
48+-- | Write UTF-8 text to the native application's standard output and flush it.
49+-- | Native I/O failures reject the Aff.
50+stdout :: String -> Aff Unit
51+stdout text = toAffE (stdoutImpl text)
52+
53+-- | Write UTF-8 text to the native application's standard error and flush it.
54+-- | Native I/O failures reject the Aff.
55+stderr :: String -> Aff Unit
56+stderr text = toAffE (stderrImpl text)
57+
4758-- | An opaque stream owned by the webview that spawned it.
4859foreign import data Stream :: Type
4960
@@ -52,3 +63,5 @@ foreign import spawnImpl :: { executable :: String, arguments :: Array String }
5263foreign import writeImpl :: Stream -> ArrayBuffer -> Effect (Promise Unit)
5364foreign import finishImpl :: Stream -> Effect (Promise Result)
5465foreign import cancelImpl :: Stream -> Effect (Promise Unit)
66+foreign import stdoutImpl :: String -> Effect (Promise Unit)
67+foreign import stderrImpl :: String -> Effect (Promise Unit)
src/Tauri/Window.js+4
@@ -2,6 +2,10 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
22
33export const setTitleImpl = title => () => getCurrentWindow().setTitle(title);
44
5+export const showImpl = () => getCurrentWindow().show();
6+
7+export const setFocusImpl = () => getCurrentWindow().setFocus();
8+
59export const onCloseRequestedImpl = handler => () =>
610 getCurrentWindow().onCloseRequested(event => handler(event)());
711
src/Tauri/Window.purs+13−1
@@ -1,6 +1,7 @@
11-- | Native operations require core:window:allow-set-title,
2+-- | core:window:allow-show, core:window:allow-set-focus,
23-- | core:window:allow-destroy and core:event:allow-listen/allow-unlisten.
3−module Tauri.Window (CloseRequest, setTitle, onCloseRequested, preventClose, destroy) where
4+module Tauri.Window (CloseRequest, setTitle, show, setFocus, onCloseRequested, preventClose, destroy) where
45
56import Prelude
67
@@ -13,6 +14,14 @@ import Tauri.Internal.Resource (acquireDisposer)
1314setTitle :: String -> Aff Unit
1415setTitle = toAffE <<< setTitleImpl
1516
17+-- | Make the current native window visible.
18+show :: Aff Unit
19+show = toAffE showImpl
20+
21+-- | Move keyboard focus to the current native window.
22+setFocus :: Aff Unit
23+setFocus = toAffE setFocusImpl
24+
1625-- | Call preventClose synchronously inside the callback BEFORE launching an
1726-- | Aff for confirmation/save work. After that work succeeds, call destroy.
1827-- | Without prevention, Tauri destroys the window when this callback returns.
@@ -32,4 +41,7 @@ foreign import setTitleImpl :: String -> Effect (Promise Unit)
3241foreign import onCloseRequestedImpl
3342 :: (CloseRequest -> Effect Unit)
3443 -> Effect (Promise (Effect Unit))
44+
3545foreign import destroyImpl :: Effect (Promise Unit)
46+foreign import showImpl :: Effect (Promise Unit)
47+foreign import setFocusImpl :: Effect (Promise Unit)