diff options
| author | Ellie Huxtable <ellie@atuin.sh> | 2024-07-08 11:17:47 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-08 11:17:47 +0100 |
| commit | 5b384487331eaf08031dfe438bb2affa31aafcbb (patch) | |
| tree | 51904c3df8c54cbc5b7aa5832a5bae49d57f7141 /ui/backend/src/pty.rs | |
| parent | feat(bash/blesh): hook into BLE_ONLOAD to resolve loading order issue (#2234) (diff) | |
| download | atuin-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 '')
| -rw-r--r-- | ui/backend/src/pty.rs | 112 |
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 + } +} |
