aboutsummaryrefslogtreecommitdiffstats
path: root/ui/backend/src/pty.rs
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@atuin.sh>2024-07-08 11:17:47 +0100
committerGitHub <noreply@github.com>2024-07-08 11:17:47 +0100
commit5b384487331eaf08031dfe438bb2affa31aafcbb (patch)
tree51904c3df8c54cbc5b7aa5832a5bae49d57f7141 /ui/backend/src/pty.rs
parentfeat(bash/blesh): hook into BLE_ONLOAD to resolve loading order issue (#2234) (diff)
downloadatuin-5b384487331eaf08031dfe438bb2affa31aafcbb.zip
feat(gui): runbooks that run (#2233)
* add initial runbooks frontend * fix buttons, scroll, add shell support to editor * work * some tweaks * wip - run crate * functioning executable blocks * handle resizing, killing ptys * clear properly on stop * move terminal to its own component, handle lifecycle better * fix all build issues * ffs codespelll * update lockfile * clippy is needy once more * only build pty stuff on mac/linux * vendor pty handling into desktop * update lockfile
Diffstat (limited to 'ui/backend/src/pty.rs')
-rw-r--r--ui/backend/src/pty.rs112
1 files changed, 112 insertions, 0 deletions
diff --git a/ui/backend/src/pty.rs b/ui/backend/src/pty.rs
new file mode 100644
index 00000000..07857824
--- /dev/null
+++ b/ui/backend/src/pty.rs
@@ -0,0 +1,112 @@
+use std::{
+ io::Write,
+ sync::{Arc, Mutex},
+};
+
+use bytes::Bytes;
+use eyre::{eyre, Result};
+use portable_pty::{CommandBuilder, MasterPty, PtySize};
+
+pub struct Pty {
+ tx: tokio::sync::mpsc::Sender<Bytes>,
+
+ pub master: Arc<Mutex<Box<dyn MasterPty + Send>>>,
+ pub reader: Arc<Mutex<Box<dyn std::io::Read + Send>>>,
+}
+
+impl Pty {
+ pub async fn open<'a>(rows: u16, cols: u16) -> Result<Self> {
+ let sys = portable_pty::native_pty_system();
+
+ let pair = sys
+ .openpty(PtySize {
+ rows,
+ cols,
+ pixel_width: 0,
+ pixel_height: 0,
+ })
+ .map_err(|e| eyre!("Failed to open pty: {}", e))?;
+
+ let cmd = CommandBuilder::new_default_prog();
+
+ tokio::task::spawn_blocking(move || {
+ let mut child = pair.slave.spawn_command(cmd).unwrap();
+ // Wait for the child to exit
+ let _ = child.wait().unwrap();
+
+ // Ensure slave is dropped
+ // This closes file handles, we can deadlock if this is not done correctly.
+ drop(pair.slave);
+ });
+
+ // Handle input -> write to master writer
+ let (master_tx, mut master_rx) = tokio::sync::mpsc::channel::<Bytes>(32);
+
+ let mut writer = pair.master.take_writer().unwrap();
+ let reader = pair
+ .master
+ .try_clone_reader()
+ .map_err(|e| e.to_string())
+ .expect("Failed to clone reader");
+
+ tokio::spawn(async move {
+ while let Some(bytes) = master_rx.recv().await {
+ writer.write_all(&bytes).unwrap();
+ writer.flush().unwrap();
+ }
+
+ // When the channel has been closed, we won't be getting any more input. Close the
+ // writer and the master.
+ // This will also close the writer, which sends EOF to the underlying shell. Ensuring
+ // that is also closed.
+ drop(writer);
+ });
+
+ Ok(Pty {
+ tx: master_tx,
+ master: Arc::new(Mutex::new(pair.master)),
+ reader: Arc::new(Mutex::new(reader)),
+ })
+ }
+
+ pub async fn resize(&self, rows: u16, cols: u16) -> Result<()> {
+ let master = self
+ .master
+ .lock()
+ .map_err(|e| eyre!("Failed to lock pty master: {e}"))?;
+
+ master
+ .resize(PtySize {
+ rows,
+ cols,
+ pixel_width: 0,
+ pixel_height: 0,
+ })
+ .map_err(|e| eyre!("Failed to resize terminal: {e}"))?;
+
+ Ok(())
+ }
+
+ pub async fn send_bytes(&self, bytes: Bytes) -> Result<()> {
+ self.tx
+ .send(bytes)
+ .await
+ .map_err(|e| eyre!("Failed to write to master tx: {}", e))
+ }
+
+ pub async fn send_string(&self, cmd: &str) -> Result<()> {
+ let bytes: Vec<u8> = cmd.bytes().collect();
+ let bytes = Bytes::from(bytes);
+
+ self.send_bytes(bytes).await
+ }
+
+ pub async fn send_single_string(&self, cmd: &str) -> Result<()> {
+ let mut bytes: Vec<u8> = cmd.bytes().collect();
+ bytes.push(0x04);
+
+ let bytes = Bytes::from(bytes);
+
+ self.send_bytes(bytes).await
+ }
+}