aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-30 01:09:48 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-30 01:10:28 +0300
commitbffc4a82c81293f9836b375d6d5ef9f4b1674a95 (patch)
treee14b6b9bd35449bd2f2383724cdb8b5ed03bf50a /src/client
parent86857c903cde90c30db3feb52da66f5c31845a9a (diff)
feat: add terminal session observe stream
Diffstat (limited to 'src/client')
-rw-r--r--src/client/mod.rs78
1 files changed, 78 insertions, 0 deletions
diff --git a/src/client/mod.rs b/src/client/mod.rs
index d9d73b95..98105781 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -20,6 +20,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
+use base64::Engine;
use crossterm::event::{
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableFocusChange, EnableMouseCapture,
@@ -814,6 +815,83 @@ pub fn run_terminal_attach(_terminal_id: String, _takeover: bool) -> io::Result<
))
}
+/// Runs a read-only terminal session observer and prints one JSON envelope per frame.
+pub fn run_terminal_session_observe(target: String, cols: u16, rows: u16) -> io::Result<()> {
+ init_logging();
+
+ let socket_path = client_socket_path();
+ crate::logging::startup("client");
+ info!(path = %socket_path.display(), target = %target, cols, rows, "observing terminal session");
+
+ let mut stream = match crate::ipc::connect_local_stream(&socket_path) {
+ Ok(stream) => stream,
+ Err(err) => {
+ eprintln!("herdr: {}", ClientError::ConnectionFailed(err));
+ std::process::exit(1);
+ }
+ };
+
+ match do_handshake(
+ &mut stream,
+ cols,
+ rows,
+ 0,
+ 0,
+ RenderEncoding::TerminalAnsi,
+ true,
+ ) {
+ Ok(RenderEncoding::TerminalAnsi) => {}
+ Ok(encoding) => {
+ eprintln!(
+ "herdr: terminal session observe negotiated unsupported encoding {encoding:?}"
+ );
+ std::process::exit(1);
+ }
+ Err(err) => {
+ eprintln!("herdr: {err}");
+ std::process::exit(1);
+ }
+ }
+
+ write_to_server(&mut stream, &ClientMessage::ObserveTerminal { target })?;
+ stream.set_nonblocking(false)?;
+
+ let mut stdout = io::stdout().lock();
+ loop {
+ match protocol::read_message(&mut stream, MAX_GRAPHICS_FRAME_SIZE) {
+ Ok(ServerMessage::Terminal(frame)) => {
+ let encoded = base64::engine::general_purpose::STANDARD.encode(&frame.bytes);
+ let line = serde_json::json!({
+ "type": "terminal.frame",
+ "seq": frame.seq,
+ "encoding": "ansi",
+ "width": frame.width,
+ "height": frame.height,
+ "full": frame.full,
+ "bytes": encoded,
+ });
+ serde_json::to_writer(&mut stdout, &line)?;
+ stdout.write_all(b"\n")?;
+ stdout.flush()?;
+ }
+ Ok(ServerMessage::ServerShutdown { reason }) => {
+ let line = serde_json::json!({
+ "type": "terminal.closed",
+ "reason": reason,
+ });
+ serde_json::to_writer(&mut stdout, &line)?;
+ stdout.write_all(b"\n")?;
+ stdout.flush()?;
+ return Ok(());
+ }
+ Ok(ServerMessage::Graphics { .. }) => {}
+ Ok(_) => {}
+ Err(protocol::FramingError::UnexpectedEof) => return Ok(()),
+ Err(err) => return Err(io::Error::other(err.to_string())),
+ }
+ }
+}
+
fn run_client_with_mode(
requested_encoding: RenderEncoding,
attach_request: Option<(String, bool)>,