tauri_plugin_pty/
lib.rs

1use std::{
2    collections::BTreeMap,
3    ffi::OsString,
4    sync::{
5        atomic::{AtomicU32, Ordering},
6        Arc,
7    },
8};
9
10use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, PtyPair, PtySize};
11use tauri::{
12    async_runtime::{Mutex, RwLock},
13    plugin::{Builder, TauriPlugin},
14    AppHandle, Manager, Runtime,
15};
16
17#[derive(Default)]
18struct PluginState {
19    session_id: AtomicU32,
20    sessions: RwLock<BTreeMap<PtyHandler, Arc<Session>>>,
21}
22
23struct Session {
24    pair: Mutex<PtyPair>,
25    child: Mutex<Box<dyn Child + Send + Sync>>,
26    child_killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,
27    writer: Mutex<Box<dyn std::io::Write + Send>>,
28    reader: Mutex<Box<dyn std::io::Read + Send>>,
29}
30
31type PtyHandler = u32;
32
33#[tauri::command]
34async fn spawn<R: Runtime>(
35    file: String,
36    args: Vec<String>,
37    term_name: Option<String>,
38    cols: u16,
39    rows: u16,
40    cwd: Option<String>,
41    env: BTreeMap<String, String>,
42    encoding: Option<String>,
43    handle_flow_control: Option<bool>,
44    flow_control_pause: Option<String>,
45    flow_control_resume: Option<String>,
46
47    state: tauri::State<'_, PluginState>,
48    _app_handle: AppHandle<R>,
49) -> Result<PtyHandler, String> {
50    // TODO: Support these parameters
51    let _ = term_name;
52    let _ = encoding;
53    let _ = handle_flow_control;
54    let _ = flow_control_pause;
55    let _ = flow_control_resume;
56
57    let pty_system = native_pty_system();
58    // Create PTY, get the writer and reader
59    let pair = pty_system
60        .openpty(PtySize {
61            rows,
62            cols,
63            pixel_width: 0,
64            pixel_height: 0,
65        })
66        .map_err(|e| e.to_string())?;
67    let writer = pair.master.take_writer().map_err(|e| e.to_string())?;
68    let reader = pair.master.try_clone_reader().map_err(|e| e.to_string())?;
69
70    let mut cmd = CommandBuilder::new(file);
71    cmd.args(args);
72    if let Some(cwd) = cwd {
73        cmd.cwd(OsString::from(cwd));
74    }
75    for (k, v) in env.iter() {
76        cmd.env(OsString::from(k), OsString::from(v));
77    }
78    let child = pair.slave.spawn_command(cmd).map_err(|e| e.to_string())?;
79    let child_killer = child.clone_killer();
80    let handler = state.session_id.fetch_add(1, Ordering::Relaxed);
81
82    let pair = Arc::new(Session {
83        pair: Mutex::new(pair),
84        child: Mutex::new(child),
85        child_killer: Mutex::new(child_killer),
86        writer: Mutex::new(writer),
87        reader: Mutex::new(reader),
88    });
89    state.sessions.write().await.insert(handler, pair);
90    Ok(handler)
91}
92
93#[tauri::command]
94async fn write(
95    pid: PtyHandler,
96    data: String,
97    state: tauri::State<'_, PluginState>,
98) -> Result<(), String> {
99    let session = state
100        .sessions
101        .read()
102        .await
103        .get(&pid)
104        .ok_or("Unavaliable pid")?
105        .clone();
106    session
107        .writer
108        .lock()
109        .await
110        .write_all(data.as_bytes())
111        .map_err(|e| e.to_string())?;
112    Ok(())
113}
114
115#[tauri::command]
116async fn read(pid: PtyHandler, state: tauri::State<'_, PluginState>) -> Result<Vec<u8>, String> {
117    let session = state
118        .sessions
119        .read()
120        .await
121        .get(&pid)
122        .ok_or("Unavaliable pid")?
123        .clone();
124    let mut buf = vec![0u8; 4096];
125    let n = session
126        .reader
127        .lock()
128        .await
129        .read(&mut buf)
130        .map_err(|e| e.to_string())?;
131    if n == 0 {
132        Err(String::from("EOF"))
133    } else {
134        buf.truncate(n);
135        Ok(buf)
136    }
137}
138
139#[tauri::command]
140async fn resize(
141    pid: PtyHandler,
142    cols: u16,
143    rows: u16,
144    state: tauri::State<'_, PluginState>,
145) -> Result<(), String> {
146    let session = state
147        .sessions
148        .read()
149        .await
150        .get(&pid)
151        .ok_or("Unavaliable pid")?
152        .clone();
153    session
154        .pair
155        .lock()
156        .await
157        .master
158        .resize(PtySize {
159            rows,
160            cols,
161            pixel_width: 0,
162            pixel_height: 0,
163        })
164        .map_err(|e| e.to_string())?;
165    Ok(())
166}
167
168#[tauri::command]
169async fn kill(pid: PtyHandler, state: tauri::State<'_, PluginState>) -> Result<(), String> {
170    let session = state
171        .sessions
172        .read()
173        .await
174        .get(&pid)
175        .ok_or("Unavaliable pid")?
176        .clone();
177    session
178        .child_killer
179        .lock()
180        .await
181        .kill()
182        .map_err(|e| e.to_string())?;
183    Ok(())
184}
185
186#[tauri::command]
187async fn exitstatus(pid: PtyHandler, state: tauri::State<'_, PluginState>) -> Result<u32, String> {
188    let session = state
189        .sessions
190        .read()
191        .await
192        .get(&pid)
193        .ok_or("Unavaliable pid")?
194        .clone();
195    let exitstatus = session
196        .child
197        .lock()
198        .await
199        .wait()
200        .map_err(|e| e.to_string())?
201        .exit_code();
202    Ok(exitstatus)
203}
204
205/// Initializes the plugin.
206pub fn init<R: Runtime>() -> TauriPlugin<R> {
207    Builder::<R>::new("pty")
208        .invoke_handler(tauri::generate_handler![
209            spawn, write, read, resize, kill, exitstatus
210        ])
211        .setup(|app_handle, _api| {
212            app_handle.manage(PluginState::default());
213            Ok(())
214        })
215        .build()
216}