Selaa lähdekoodia

refactor(tauri) execute_promise String/Serialize management (#724)

Lucas Fernandes Nogueira 5 vuotta sitten
vanhempi
sitoutus
6b097345ed

+ 8 - 0
.changes/execute-promise-refactor.md

@@ -0,0 +1,8 @@
+---
+"tauri-api": minor
+"tauri": minor
+---
+
+The `execute_promise` and `execute_promise_sync` helpers now accepts any `tauri::Result<T>` where `T: impl Serialize`.
+This means that you do not need to serialize your response manually or deal with String quotes anymore.
+As part of this refactor, the `event::emit` function also supports `impl Serialize` instead of `String`.

+ 1 - 1
cli/tauri.js/api-src/types/http.ts

@@ -18,7 +18,7 @@ export interface HttpOptions {
   method: HttpVerb
   url: string
   headers?: Record<string, any>
-  propertys?: Record<string, any>
+  params?: Record<string, any>
   body?: Body
   followRedirects: boolean
   maxRedirections: boolean

+ 4 - 7
tauri-api/src/http.rs

@@ -221,7 +221,7 @@ impl HttpRequestBuilder {
 ///
 /// The response will be transformed to String,
 /// If reading the response as binary, the byte array will be serialized using serde_json
-pub fn make_request(options: HttpRequestOptions) -> crate::Result<String> {
+pub fn make_request(options: HttpRequestOptions) -> crate::Result<Value> {
   let method = Method::from_bytes(options.method.to_uppercase().as_bytes())?;
   let mut builder = RequestBuilder::new(method, options.url);
   if let Some(params) = options.params {
@@ -291,12 +291,9 @@ pub fn make_request(options: HttpRequestOptions) -> crate::Result<String> {
   let response = response?;
   if response.is_success() {
     let response_data = match options.response_type.unwrap_or(ResponseType::Json) {
-      ResponseType::Json => {
-        let result = response.json::<Value>()?;
-        serde_json::to_string(&result)?
-      }
-      ResponseType::Text => response.text()?,
-      ResponseType::Binary => serde_json::to_string(&response.bytes()?)?,
+      ResponseType::Json => response.json::<Value>()?,
+      ResponseType::Text => Value::String(response.text()?),
+      ResponseType::Binary => Value::String(serde_json::to_string(&response.bytes()?)?),
     };
     Ok(response_data)
   } else {

+ 2 - 5
tauri-api/src/notification.rs

@@ -16,6 +16,7 @@ use std::path::MAIN_SEPARATOR;
 ///   .show();
 /// ```
 #[allow(dead_code)]
+#[derive(Default)]
 pub struct Notification {
   /// The notification body.
   body: Option<String>,
@@ -28,11 +29,7 @@ pub struct Notification {
 impl Notification {
   /// Initializes a instance of a Notification.
   pub fn new() -> Self {
-    Self {
-      body: None,
-      title: None,
-      icon: None,
-    }
+    Default::default()
   }
 
   /// Sets the notification body.

+ 53 - 61
tauri-api/src/rpc.rs

@@ -1,12 +1,15 @@
-/// Formats a function to be evaluated as callback.
-/// If the arg is a string literal, it needs the proper quotes.
+use serde::Serialize;
+use serde_json::Value as JsonValue;
+use std::fmt::Display;
+
+/// Formats a function name and argument to be evaluated as callback.
 ///
 /// # Examples
 /// ```
 /// use tauri_api::rpc::format_callback;
 /// // callback with a string argument
-/// // returns `window["callback-function-name"]("the string response")`
-/// format_callback("callback-function-name".to_string(), r#""the string response""#.to_string());
+/// let cb = format_callback("callback-function-name", "the string response");
+/// assert_eq!(cb, r#"window["callback-function-name"]("the string response")"#);
 /// ```
 ///
 /// ```
@@ -17,39 +20,50 @@
 /// struct MyResponse {
 ///   value: String
 /// }
-/// // this returns `window["callback-function-name"]({value: "some value"})`
-/// format_callback("callback-function-name".to_string(), serde_json::to_string(&MyResponse {
+/// let cb = format_callback("callback-function-name", serde_json::to_value(&MyResponse {
 ///   value: "some value".to_string()
-/// }).expect("failed to serialize type"));
+/// }).expect("failed to serialize"));
+/// assert_eq!(cb, r#"window["callback-function-name"]({"value":"some value"})"#);
 /// ```
-pub fn format_callback(function_name: String, arg: String) -> String {
-  let formatted_string = &format!("window[\"{}\"]({})", function_name, arg);
-  formatted_string.to_string()
+pub fn format_callback<T: Into<JsonValue>, S: AsRef<str> + Display>(
+  function_name: S,
+  arg: T,
+) -> String {
+  format!(r#"window["{}"]({})"#, function_name, arg.into().to_string())
 }
 
-/// Formats a Result type to its callback version.
+/// Formats a Result type to its Promise response.
 /// Useful for Promises handling.
+/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
+/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
+///
+/// * `result` the Result to check
+/// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise.
+/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
 ///
-/// If the Result is Ok, `format_callback` will be called directly.
-/// If the result is an Err, we assume the error message is a string, and quote it.
+/// Note that the callback strings are automatically generated by the `promisified` helper.
 ///
 /// # Examples
 /// ```
 /// use tauri_api::rpc::format_callback_result;
-/// // returns `window["success_cb"](5)`
-/// format_callback_result(Ok("5".to_string()), "success_cb".to_string(), "error_cb".to_string());
-/// // returns `window["error_cb"]("error message here")`
-/// format_callback_result(Err("error message here".to_string()), "success_cb".to_string(), "error_cb".to_string());
+/// let res: Result<u8, &str> = Ok(5);
+/// let cb = format_callback_result(res, "success_cb".to_string(), "error_cb".to_string()).expect("failed to format");
+/// assert_eq!(cb, r#"window["success_cb"](5)"#);
+///
+/// let res: Result<&str, &str> = Err("error message here");
+/// let cb = format_callback_result(res, "success_cb".to_string(), "error_cb".to_string()).expect("failed to format");
+/// assert_eq!(cb, r#"window["error_cb"]("error message here")"#);
 /// ```
-pub fn format_callback_result(
-  result: Result<String, String>,
-  callback: String,
+pub fn format_callback_result<T: Serialize, E: Serialize>(
+  result: Result<T, E>,
+  success_callback: String,
   error_callback: String,
-) -> String {
-  match result {
-    Ok(res) => format_callback(callback, res),
-    Err(err) => format_callback(error_callback, format!("\"{}\"", err)),
-  }
+) -> crate::Result<String> {
+  let rpc = match result {
+    Ok(res) => format_callback(success_callback, serde_json::to_value(res)?),
+    Err(err) => format_callback(error_callback, serde_json::to_value(err)?),
+  };
+  Ok(rpc)
 }
 
 #[cfg(test)]
@@ -62,16 +76,9 @@ mod test {
   fn qc_formating(f: String, a: String) -> bool {
     // can not accept empty strings
     if f != "" && a != "" {
-      // get length of function and argument
-      let alen = &a.len();
-      let flen = &f.len();
       // call format callback
-      let fc = format_callback(f, a);
-      // get length of the resulting string
-      let fclen = fc.len();
-
-      // if formatted string equals the length of the argument and the function plus 12 then its correct.
-      fclen == alen + flen + 12
+      let fc = format_callback(f.clone(), a.clone());
+      fc == format!(r#"window["{}"]({})"#, f, serde_json::Value::String(a))
     } else {
       true
     }
@@ -80,33 +87,18 @@ mod test {
   // check arbitrary strings in format_callback_result
   #[quickcheck]
   fn qc_format_res(result: Result<String, String>, c: String, ec: String) -> bool {
-    // match on result to decide how to call the function.
-    match result {
-      // if ok, get length of result and callback strings.
-      Ok(r) => {
-        let rlen = r.len();
-        let clen = c.len();
-
-        // take the ok string from result and pass it into format_callback_result as an ok.
-        let resp = format_callback_result(Ok(r), c, ec);
-        // get response string length
-        let reslen = resp.len();
+    let resp = format_callback_result(result.clone(), c.clone(), ec.clone())
+      .expect("failed to format callback result");
+    let (function, value) = match result {
+      Ok(v) => (c, v),
+      Err(e) => (ec, e),
+    };
 
-        // if response string length equals result and callback length plus 12 characters then it is correct.
-        reslen == rlen + clen + 12
-      }
-      // If Err, get length of Err and Error callback
-      Err(err) => {
-        let eclen = ec.len();
-        let errlen = err.len();
-        // pass err as Err into format_callback_result with callback and error callback
-        let resp = format_callback_result(Err(err), c, ec);
-        // get response string length
-        let reslen = resp.len();
-
-        // if length of response string equals the error length and the error callback length plus 14 characters then its is correct.
-        reslen == eclen + errlen + 14
-      }
-    }
+    resp
+      == format!(
+        r#"window["{}"]({})"#,
+        function,
+        serde_json::Value::String(value),
+      )
   }
 }

+ 5 - 5
tauri/src/endpoints.rs

@@ -179,7 +179,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
           callback,
           error,
         } => {
-          salt::validate(webview, salt, callback, error);
+          salt::validate(webview, salt, callback, error)?;
         }
         Listen {
           event,
@@ -206,7 +206,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
           error,
         } => {
           #[cfg(open_dialog)]
-          dialog::open(webview, options, callback, error);
+          dialog::open(webview, options, callback, error)?;
           #[cfg(not(open_dialog))]
           whitelist_error(webview, error, "title");
         }
@@ -216,7 +216,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
           error,
         } => {
           #[cfg(save_dialog)]
-          dialog::save(webview, options, callback, error);
+          dialog::save(webview, options, callback, error)?;
           #[cfg(not(save_dialog))]
           throw_whitelist_error(webview, "saveDialog");
         }
@@ -244,7 +244,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
           crate::execute_promise(
             webview,
             move || match crate::cli::get_matches() {
-              Some(matches) => Ok(serde_json::to_string(matches)?),
+              Some(matches) => Ok(matches),
               None => Err(anyhow::anyhow!(r#""failed to get matches""#)),
             },
             callback,
@@ -271,7 +271,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
         }
         RequestNotificationPermission { callback, error } => {
           #[cfg(notification)]
-          notification::request_permission(webview, callback, error);
+          notification::request_permission(webview, callback, error)?;
           #[cfg(not(notification))]
           whitelist_error(webview, error, "notification");
         }

+ 1 - 1
tauri/src/endpoints/asset.rs

@@ -70,7 +70,7 @@ pub fn load<T: 'static>(
             }
           })
           .map_err(|err| err.into())
-          .map(|_| r#""Asset loaded successfully""#.to_string())
+          .map(|_| "Asset loaded successfully".to_string())
       }
     },
     callback,

+ 11 - 8
tauri/src/endpoints/dialog.rs

@@ -1,13 +1,14 @@
 use super::cmd::{OpenDialogOptions, SaveDialogOptions};
 use crate::api::dialog::{pick_folder, save_file, select, select_multiple, Response};
+use serde_json::Value as JsonValue;
 use web_view::WebView;
 
 /// maps a dialog response to a JS value to eval
-fn map_response(response: Response) -> String {
+fn map_response(response: Response) -> JsonValue {
   match response {
-    Response::Okay(path) => format!(r#""{}""#, path).replace("\\", "\\\\"),
-    Response::OkayMultiple(paths) => format!("{:?}", paths),
-    Response::Cancel => panic!("unexpected response type"),
+    Response::Okay(path) => path.into(),
+    Response::OkayMultiple(paths) => paths.into(),
+    Response::Cancel => JsonValue::Null,
   }
 }
 
@@ -18,7 +19,7 @@ pub fn open<T: 'static>(
   options: OpenDialogOptions,
   callback: String,
   error: String,
-) {
+) -> crate::Result<()> {
   crate::execute_promise_sync(
     webview,
     move || {
@@ -33,7 +34,8 @@ pub fn open<T: 'static>(
     },
     callback,
     error,
-  );
+  )?;
+  Ok(())
 }
 
 /// Shows a save dialog.
@@ -43,11 +45,12 @@ pub fn save<T: 'static>(
   options: SaveDialogOptions,
   callback: String,
   error: String,
-) {
+) -> crate::Result<()> {
   crate::execute_promise_sync(
     webview,
     move || save_file(options.filter, options.default_path).map(map_response),
     callback,
     error,
-  );
+  )?;
+  Ok(())
 }

+ 9 - 30
tauri/src/endpoints/file_system.rs

@@ -28,7 +28,6 @@ pub fn read_dir<T: 'static>(
         (false, None)
       };
       dir::read_dir(resolve_path(path, dir)?, recursive)
-        .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
     },
     callback,
     error,
@@ -55,9 +54,7 @@ pub fn copy_file<T: 'static>(
         ),
         None => (source, destination),
       };
-      fs::copy(src, dest)
-        .map_err(|e| e.into())
-        .map(|_| "".to_string())
+      fs::copy(src, dest).map_err(|e| e.into())
     },
     callback,
     error,
@@ -88,7 +85,7 @@ pub fn create_dir<T: 'static>(
         fs::create_dir(resolved_path)
       };
 
-      response.map_err(|e| e.into()).map(|_| "".to_string())
+      response.map_err(|e| e.into())
     },
     callback,
     error,
@@ -119,7 +116,7 @@ pub fn remove_dir<T: 'static>(
         fs::remove_dir(resolved_path)
       };
 
-      response.map_err(|e| e.into()).map(|_| "".to_string())
+      response.map_err(|e| e.into())
     },
     callback,
     error,
@@ -139,9 +136,7 @@ pub fn remove_file<T: 'static>(
     webview,
     move || {
       let resolved_path = resolve_path(path, options.and_then(|o| o.dir))?;
-      fs::remove_file(resolved_path)
-        .map_err(|e| e.into())
-        .map(|_| "".to_string())
+      fs::remove_file(resolved_path).map_err(|e| e.into())
     },
     callback,
     error,
@@ -168,9 +163,7 @@ pub fn rename_file<T: 'static>(
         ),
         None => (old_path, new_path),
       };
-      fs::rename(old, new)
-        .map_err(|e| e.into())
-        .map(|_| "".to_string())
+      fs::rename(old, new).map_err(|e| e.into())
     },
     callback,
     error,
@@ -192,11 +185,7 @@ pub fn write_file<T: 'static>(
     move || {
       File::create(resolve_path(file, options.and_then(|o| o.dir))?)
         .map_err(|e| e.into())
-        .and_then(|mut f| {
-          f.write_all(contents.as_bytes())
-            .map_err(|err| err.into())
-            .map(|_| "".to_string())
-        })
+        .and_then(|mut f| f.write_all(contents.as_bytes()).map_err(|err| err.into()))
     },
     callback,
     error,
@@ -221,11 +210,7 @@ pub fn write_binary_file<T: 'static>(
         .and_then(|c| {
           File::create(resolve_path(file, options.and_then(|o| o.dir))?)
             .map_err(|e| e.into())
-            .and_then(|mut f| {
-              f.write_all(&c)
-                .map_err(|err| err.into())
-                .map(|_| "".to_string())
-            })
+            .and_then(|mut f| f.write_all(&c).map_err(|err| err.into()))
         })
     },
     callback,
@@ -244,10 +229,7 @@ pub fn read_text_file<T: 'static>(
 ) {
   crate::execute_promise(
     webview,
-    move || {
-      file::read_string(resolve_path(path, options.and_then(|o| o.dir))?)
-        .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
-    },
+    move || file::read_string(resolve_path(path, options.and_then(|o| o.dir))?),
     callback,
     error,
   );
@@ -264,10 +246,7 @@ pub fn read_binary_file<T: 'static>(
 ) {
   crate::execute_promise(
     webview,
-    move || {
-      file::read_binary(resolve_path(path, options.and_then(|o| o.dir))?)
-        .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
-    },
+    move || file::read_binary(resolve_path(path, options.and_then(|o| o.dir))?),
     callback,
     error,
   );

+ 2 - 18
tauri/src/endpoints/http.rs

@@ -1,4 +1,4 @@
-use tauri_api::http::{make_request as request, HttpRequestOptions, ResponseType};
+use tauri_api::http::{make_request as request, HttpRequestOptions};
 use web_view::WebView;
 
 /// Makes an HTTP request and resolves the response to the webview
@@ -8,21 +8,5 @@ pub fn make_request<T: 'static>(
   callback: String,
   error: String,
 ) {
-  crate::execute_promise(
-    webview,
-    move || {
-      let response_type = options.response_type.clone();
-      request(options).map(
-        |response| match response_type.unwrap_or(ResponseType::Json) {
-          ResponseType::Text => format!(
-            r#""{}""#,
-            response.replace(r#"""#, r#"\""#).replace(r#"\\""#, r#"\""#)
-          ),
-          _ => response,
-        },
-      )
-    },
-    callback,
-    error,
-  );
+  crate::execute_promise(webview, move || request(options), callback, error);
 }

+ 7 - 7
tauri/src/endpoints/notification.rs

@@ -1,4 +1,5 @@
 use super::cmd::NotificationOptions;
+use serde_json::Value as JsonValue;
 use web_view::WebView;
 
 pub fn send<T: 'static>(
@@ -18,9 +19,7 @@ pub fn send<T: 'static>(
       if let Some(icon) = options.icon {
         notification = notification.icon(icon);
       }
-      notification
-        .show()
-        .map_err(|e| anyhow::anyhow!(r#""{}""#, e.to_string()))?;
+      notification.show()?;
       Ok("".to_string())
     },
     callback,
@@ -38,9 +37,9 @@ pub fn is_permission_granted<T: 'static>(
     move || {
       let settings = crate::settings::read_settings()?;
       if let Some(allow_notification) = settings.allow_notification {
-        Ok(allow_notification.to_string())
+        Ok(JsonValue::String(allow_notification.to_string()))
       } else {
-        Ok("null".to_string())
+        Ok(JsonValue::Null)
       }
     },
     callback,
@@ -52,7 +51,7 @@ pub fn request_permission<T: 'static>(
   webview: &mut WebView<'_, T>,
   callback: String,
   error: String,
-) {
+) -> crate::Result<()> {
   crate::execute_promise_sync(
     webview,
     move || {
@@ -82,5 +81,6 @@ pub fn request_permission<T: 'static>(
     },
     callback,
     error,
-  );
+  )?;
+  Ok(())
 }

+ 4 - 5
tauri/src/endpoints/salt.rs

@@ -6,14 +6,13 @@ pub fn validate<T: 'static>(
   salt: String,
   callback: String,
   error: String,
-) {
+) -> crate::Result<()> {
   let response = if crate::salt::is_valid(salt) {
     Ok("'VALID'".to_string())
   } else {
     Err("'INVALID SALT'".to_string())
   };
-  let callback_string = crate::api::rpc::format_callback_result(response, callback, error);
-  webview
-    .eval(callback_string.as_str())
-    .expect("Failed to eval JS from validate()");
+  let callback_string = crate::api::rpc::format_callback_result(response, callback, error)?;
+  webview.eval(callback_string.as_str())?;
+  Ok(())
 }

+ 12 - 4
tauri/src/event.rs

@@ -4,6 +4,8 @@ use std::sync::{Arc, Mutex};
 
 use lazy_static::lazy_static;
 use once_cell::sync::Lazy;
+use serde::Serialize;
+use serde_json::Value as JsonValue;
 use web_view::Handle;
 
 /// An event handler.
@@ -55,13 +57,17 @@ pub fn listen<F: FnMut(Option<String>) + Send + 'static>(id: String, handler: F)
 }
 
 /// Emits an event to JS.
-pub fn emit<T: 'static>(webview_handle: &Handle<T>, event: String, payload: Option<String>) {
+pub fn emit<T: 'static, S: Serialize>(
+  webview_handle: &Handle<T>,
+  event: String,
+  payload: Option<S>,
+) -> crate::Result<()> {
   let salt = crate::salt::generate();
 
-  let js_payload = if let Some(payload_str) = payload {
-    payload_str
+  let js_payload = if let Some(payload_value) = payload {
+    serde_json::to_value(payload_value)?
   } else {
-    "void 0".to_string()
+    JsonValue::Null
   };
 
   webview_handle
@@ -75,6 +81,8 @@ pub fn emit<T: 'static>(webview_handle: &Handle<T>, event: String, payload: Opti
       ))
     })
     .expect("Failed to dispatch JS from emit");
+
+  Ok(())
 }
 
 /// Triggers the given event with its payload.

+ 37 - 23
tauri/src/lib.rs

@@ -32,17 +32,19 @@ mod endpoints;
 /// The salt helpers.
 mod salt;
 
-use std::process::Stdio;
-
 /// Alias for a Result with error type anyhow::Error.
 pub use anyhow::Result;
-use threadpool::ThreadPool;
-
+pub use app::*;
+pub use tauri_api as api;
 pub use web_view::Handle;
+
+use std::process::Stdio;
+
+use api::rpc::{format_callback, format_callback_result};
+use serde::Serialize;
+use threadpool::ThreadPool;
 use web_view::WebView;
 
-pub use app::*;
-pub use tauri_api as api;
 thread_local!(static POOL: ThreadPool = ThreadPool::new(4));
 
 /// Executes the operation in the thread pool.
@@ -56,33 +58,49 @@ pub fn spawn<F: FnOnce() -> () + Send + 'static>(task: F) {
 
 /// Synchronously executes the given task
 /// and evaluates its Result to the JS promise described by the `callback` and `error` function names.
-pub fn execute_promise_sync<T: 'static, F: FnOnce() -> crate::Result<String> + Send + 'static>(
+pub fn execute_promise_sync<
+  T: 'static,
+  R: Serialize,
+  F: FnOnce() -> crate::Result<R> + Send + 'static,
+>(
   webview: &mut WebView<'_, T>,
   task: F,
   callback: String,
   error: String,
-) {
+) -> crate::Result<()> {
   let handle = webview.handle();
   let callback_string =
-    api::rpc::format_callback_result(task().map_err(|err| err.to_string()), callback, error);
-  handle
-    .dispatch(move |_webview| _webview.eval(callback_string.as_str()))
-    .expect("Failed to dispatch promise callback");
+    format_callback_result(task().map_err(|err| err.to_string()), callback, error)?;
+  handle.dispatch(move |_webview| _webview.eval(callback_string.as_str()))?;
+  Ok(())
 }
 
 /// Asynchronously executes the given task
-/// and evaluates its Result to the JS promise described by the `callback` and `error` function names.
-pub fn execute_promise<T: 'static, F: FnOnce() -> crate::Result<String> + Send + 'static>(
+/// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names.
+///
+/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
+/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
+pub fn execute_promise<
+  T: 'static,
+  R: Serialize,
+  F: FnOnce() -> crate::Result<R> + Send + 'static,
+>(
   webview: &mut WebView<'_, T>,
   task: F,
-  callback: String,
-  error: String,
+  success_callback: String,
+  error_callback: String,
 ) {
   let handle = webview.handle();
   POOL.with(|thread| {
     thread.execute(move || {
-      let callback_string =
-        api::rpc::format_callback_result(task().map_err(|err| err.to_string()), callback, error);
+      let callback_string = match format_callback_result(
+        task().map_err(|err| err.to_string()),
+        success_callback,
+        error_callback.clone(),
+      ) {
+        Ok(callback_string) => callback_string,
+        Err(e) => format_callback(error_callback, e.to_string()),
+      };
       handle
         .dispatch(move |_webview| _webview.eval(callback_string.as_str()))
         .expect("Failed to dispatch promise callback")
@@ -100,11 +118,7 @@ pub fn call<T: 'static>(
 ) {
   execute_promise(
     webview,
-    || {
-      api::command::get_output(command, args, Stdio::piped())
-        .map_err(|err| err)
-        .map(|output| format!(r#""{}""#, output))
-    },
+    || api::command::get_output(command, args, Stdio::piped()),
     callback,
     error,
   );