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 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 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
205pub 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}