diff --git a/crates/embers-cli/src/lib.rs b/crates/embers-cli/src/lib.rs index 49fa198..80fee3f 100644 --- a/crates/embers-cli/src/lib.rs +++ b/crates/embers-cli/src/lib.rs @@ -90,6 +90,21 @@ pub enum Command { #[arg(long)] force: bool, }, + #[command(name = "list-buffers")] + ListBuffers { + #[arg(short = 't', long = "target")] + target: Option, + #[arg(long, conflicts_with = "detached")] + attached: bool, + #[arg(long, conflicts_with = "attached")] + detached: bool, + }, + #[command(name = "attach-buffer")] + AttachBuffer { + buffer_id: u64, + #[arg(short = 't', long = "target")] + target: Option, + }, #[command(name = "new-window")] NewWindow { #[arg(short = 't', long = "target")] @@ -243,6 +258,42 @@ async fn execute(socket: &Path, command: Command) -> Result { .await?; Ok(String::new()) } + Command::ListBuffers { + target, + attached, + detached, + } => { + let session_id = match target { + Some(target) => Some(connection.resolve_session_record(Some(&target)).await?.id), + None => None, + }; + let response = connection + .request(ClientMessage::Buffer(BufferRequest::List { + request_id: new_request_id(), + session_id, + attached_only: attached, + detached_only: detached, + })) + .await?; + match response { + ServerResponse::Buffers(response) => Ok(format_buffers(&response.buffers)), + other => Err(MuxError::protocol(format!( + "unexpected response to list-buffers: {other:?}" + ))), + } + } + Command::AttachBuffer { buffer_id, target } => { + let pane = connection.resolve_pane(target.as_deref()).await?; + let response = connection + .request(ClientMessage::Node(NodeRequest::MoveBufferToNode { + request_id: new_request_id(), + buffer_id: BufferId(buffer_id), + target_leaf_node_id: pane.leaf_id, + })) + .await?; + expect_session_snapshot(response, "attach-buffer")?; + Ok(String::new()) + } Command::NewWindow { target, title, @@ -1122,6 +1173,26 @@ fn format_sessions(sessions: &[SessionRecord]) -> String { .join("\n") } +fn format_buffers(buffers: &[embers_protocol::BufferRecord]) -> String { + buffers + .iter() + .map(|buffer| { + let attachment = buffer + .attachment_node_id + .map(|node_id| format!("attached:{node_id}")) + .unwrap_or_else(|| "detached".to_owned()); + format!( + "{}\t{}\t{}\t{}", + buffer.id, + buffer_state_label(buffer.state), + attachment, + buffer.title + ) + }) + .collect::>() + .join("\n") +} + fn format_windows(snapshot: &SessionSnapshot) -> Result { if let Some((_, tabs)) = root_tabs(snapshot)? { Ok(tabs @@ -1354,6 +1425,15 @@ fn split_scoped_target(target: Option<&str>) -> (Option, Option) } } +fn buffer_state_label(state: embers_protocol::BufferRecordState) -> &'static str { + match state { + embers_protocol::BufferRecordState::Created => "created", + embers_protocol::BufferRecordState::Running => "running", + embers_protocol::BufferRecordState::Interrupted => "interrupted", + embers_protocol::BufferRecordState::Exited => "exited", + } +} + fn split_scoped_required(target: &str, label: &str) -> Result<(Option, String)> { let (session, selector) = split_scoped_target(Some(target)); let selector = diff --git a/crates/embers-cli/tests/panes.rs b/crates/embers-cli/tests/panes.rs index 6c7456b..4e06071 100644 --- a/crates/embers-cli/tests/panes.rs +++ b/crates/embers-cli/tests/panes.rs @@ -1,5 +1,7 @@ use std::time::Duration; +use embers_core::RequestId; +use embers_protocol::{BufferRequest, ClientMessage, ServerResponse}; use embers_test_support::{TestConnection, TestServer}; use tokio::time::sleep; @@ -124,3 +126,121 @@ async fn pane_commands_round_trip_through_cli() { server.shutdown().await.expect("shutdown server"); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn detached_buffers_can_be_listed_and_attached_via_cli() { + let server = TestServer::start().await.expect("start server"); + + run_cli(&server, ["new-session", "alpha"]); + run_cli( + &server, + [ + "new-window", + "-t", + "alpha", + "--title", + "work", + "--", + "/bin/sh", + ], + ); + + let split = run_cli(&server, ["split-window", "--", "/bin/sh"]); + let target_pane_id = stdout(&split) + .trim() + .parse::() + .expect("split-window returns pane id"); + + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + let detached = connection + .request(&ClientMessage::Buffer(BufferRequest::Create { + request_id: RequestId(1), + title: Some("detached-tools".to_owned()), + command: vec!["/bin/sh".to_owned()], + cwd: None, + env: Default::default(), + })) + .await + .expect("buffer create succeeds"); + let detached_buffer_id = match detached { + ServerResponse::Buffer(response) => response.buffer.id, + other => panic!("expected buffer response, got {other:?}"), + }; + + let listed = run_cli(&server, ["list-buffers", "--detached"]); + assert!( + stdout(&listed) + .lines() + .filter(|line| line.starts_with(&format!("{detached_buffer_id}\t"))) + .any(|line| { + let fields: Vec<&str> = line.split('\t').collect(); + fields + .get(1) + .is_some_and(|status| status == &"created" || status == &"running") + }) + ); + + run_cli( + &server, + [ + "attach-buffer", + &detached_buffer_id.to_string(), + "-t", + &target_pane_id.to_string(), + ], + ); + + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + let moved_leaf = snapshot + .nodes + .iter() + .find(|node| node.id == embers_core::NodeId(target_pane_id)) + .expect("target pane exists"); + assert_eq!( + moved_leaf + .buffer_view + .as_ref() + .expect("target is buffer view") + .buffer_id, + detached_buffer_id + ); + + let listed = run_cli(&server, ["list-buffers", "--attached"]); + assert!( + stdout(&listed) + .lines() + .any(|line| line.starts_with(&format!( + "{detached_buffer_id}\trunning\tattached:{target_pane_id}\t" + ))) + ); + + let buffers = connection + .request(&ClientMessage::Buffer(BufferRequest::List { + request_id: RequestId(2), + session_id: None, + attached_only: false, + detached_only: false, + })) + .await + .expect("buffer list succeeds"); + let previous_buffer_id = match buffers { + ServerResponse::Buffers(response) => response + .buffers + .into_iter() + .find(|buffer| buffer.id != detached_buffer_id && buffer.attachment_node_id.is_none()) + .map(|buffer| buffer.id) + .expect("previous pane buffer became detached"), + other => panic!("expected buffers response, got {other:?}"), + }; + + let detached_again = run_cli(&server, ["list-buffers", "--detached"]); + assert!( + stdout(&detached_again) + .lines() + .any(|line| line.starts_with(&format!("{previous_buffer_id}\trunning\tdetached\t"))) + ); + + server.shutdown().await.expect("shutdown server"); +}