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