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