tev_client/
lib.rs

1//! This Rust crate implements a IPC TCP client for [tev](https://github.com/Tom94/tev).
2//! It enables programmatic control of the images displayed by _tev_ using a convenient and safe Rust api.
3//!
4//! Supports all existing _tev_ commands:
5//! * [PacketOpenImage](crate::PacketOpenImage) open an existing image given the path
6//! * [PacketReloadImage](crate::PacketReloadImage) reload an image from disk
7//! * [PacketCloseImage](crate::PacketCloseImage) close an opened image
8//! * [PacketCreateImage](crate::PacketCreateImage) create a new black image with given size and channels
9//! * [PacketUpdateImage](crate::PacketUpdateImage) update part of the pixels of an opened image
10//!
11//! ## Example code:
12//!
13//! ```rust
14//! use tev_client::{TevClient, TevError, PacketCreateImage};
15//!
16//! fn main() -> Result<(), TevError> {
17//!     // Spawn a tev instance, this command assumes tev is on the PATH.
18//!     // There are other constructors available too, see TevClient::spawn and TevClient::wrap.
19//!     let mut client = TevClient::spawn_path_default()?;
20//!
21//!     // Create a new image
22//!     client.send(PacketCreateImage {
23//!         image_name: "test",
24//!         grab_focus: false,
25//!         width: 1920,
26//!         height: 1080,
27//!         channel_names: &["R", "G", "B"],
28//!     })?;
29//!
30//!     Ok(())
31//! }
32//! ```
33
34use std::error::Error;
35use std::fmt::{Display, Formatter};
36use std::io;
37use std::io::{BufRead, BufReader, Write};
38use std::net::TcpStream;
39use std::process::{Command, Stdio};
40
41/// A connection to a Tev instance.
42/// Constructed using [TevClient::wrap], [TevClient::spawn] or [TevClient::spawn_path_default].
43/// Use [TevClient::send] to send commands.
44#[derive(Debug)]
45pub struct TevClient {
46    socket: TcpStream,
47}
48
49/// The error type returned by [TevClient::spawn] in case of an error.
50///
51/// For convenience, this type implements `From<std::io::Error>` so the errors returned by [TevClient::send]
52/// can be wrapped into this type by the `?` operator.
53#[derive(Debug)]
54pub enum TevError {
55    /// Error during command execution.
56    Command { io: std::io::Error },
57    /// Error while reading from stdout of the spawned process.
58    Stdout { io: std::io::Error },
59    /// Tev didn't respond with an address to connect to on stdout.
60    /// `read` is the data that was read before stdout closed.
61    NoSocketResponse { read: String },
62    /// There was an error opening or writing to the TCP connection.
63    /// `host` is the address received from _tev_ we're trying to connect to.
64    TcpConnect { host: String, io: std::io::Error },
65    /// There was some other IO error. This variant exits to make the `?` more convenient to use.
66    IO { io: std::io::Error },
67}
68
69impl TevClient {
70    /// Create a [TevClient] from an existing [TcpStream] that's connected to _tev_. If _tev_ may not be running yet use
71    /// [TevClient::spawn] or [TevClient::spawn_path_default] instead.
72    ///
73    /// For example, if _tev_ is already running on the default hostname:
74    /// ```no_run
75    /// # use tev_client::{TevClient, TevError};
76    /// # use std::net::TcpStream;
77    /// # fn main() -> std::io::Result<()> {
78    /// let mut client = TevClient::wrap(TcpStream::connect("127.0.0.1:14158")?);
79    /// # Ok(())
80    /// # }
81    /// ```
82    pub fn wrap(socket: TcpStream) -> Self {
83        TevClient { socket }
84    }
85
86    /// Create a new [TevClient] by spawning _tev_ assuming it is in `PATH` with the default hostname.
87    pub fn spawn_path_default() -> Result<TevClient, TevError> {
88        TevClient::spawn(Command::new("tev"))
89    }
90
91    /// Crate a [TevClient] from a command that spawns _tev_.
92    /// If _tev_ is in `PATH` and the default hostname should be used use [TevClient::spawn_path_default] instead.
93    ///
94    /// ```no_run
95    /// # use tev_client::{TevClient, TevError};
96    /// # use std::process::Command;
97    /// # fn main() -> Result<(), TevError> {
98    /// let mut command = Command::new("path/to/tev");
99    /// command.arg("--hostname=127.0.0.1:14159");
100    /// let mut client = TevClient::spawn(command)?;
101    /// # Ok(())
102    /// # }
103    /// ```
104    pub fn spawn(mut command: Command) -> Result<TevClient, TevError> {
105        const PATTERNS: &[&str] = &[
106            "Initialized IPC, listening on ",
107            "Connected to primary instance at ",
108        ];
109
110        let mut child = command.stdout(Stdio::piped()).spawn()
111            .map_err(|io| TevError::Command { io })?;
112        let reader = BufReader::new(child.stdout.take().unwrap());
113
114        let mut read = String::new();
115        for line in reader.lines() {
116            let line = line.map_err(|io| TevError::Stdout { io })?;
117
118            for pattern in PATTERNS {
119                if let Some(start) = line.find(pattern) {
120                    let rest = &line[start + pattern.len()..];
121
122                    // cut of any trailing terminal escape codes
123                    let end = rest.find('\u{1b}').unwrap_or(rest.len());
124                    let host = &rest[..end];
125
126                    let socket = TcpStream::connect(host)
127                        .map_err(|io| TevError::TcpConnect { host: host.to_string(), io })?;
128                    return Ok(TevClient::wrap(socket));
129                }
130            }
131
132            read.push_str(&line);
133            read.push('\n');
134        }
135
136        Err(TevError::NoSocketResponse { read })
137    }
138
139    /// Send a command to _tev_. A command is any struct in this crate that implements [TevPacket].
140    /// # Example
141    /// ```no_run
142    /// # use tev_client::{TevClient, PacketOpenImage};
143    /// # fn main() -> std::io::Result<()> {
144    /// # use tev_client::PacketCloseImage;
145    /// # let mut client: TevClient = unimplemented!();
146    /// client.send(PacketCloseImage { image_name: "test.exf" })?;
147    /// # Ok(())
148    /// # }
149    /// ```
150    pub fn send(&mut self, packet: impl TevPacket) -> io::Result<()> {
151        //reserve space for the packet length
152        let vec = vec![0, 0, 0, 0];
153
154        //append the packet
155        let mut target = TevWriter { target: vec };
156        packet.write_to(&mut target);
157        let mut vec = target.target;
158
159        //actually fill in the packet length
160        let packet_length = vec.len() as u32;
161        vec[0..4].copy_from_slice(&packet_length.to_le_bytes());
162
163        self.socket.write_all(&vec)
164    }
165}
166
167/// Opens a new image where `image_name` is the path.
168#[derive(Debug)]
169pub struct PacketOpenImage<'a> {
170    pub image_name: &'a str,
171    pub grab_focus: bool,
172    pub channel_selector: &'a str,
173}
174
175impl TevPacket for PacketOpenImage<'_> {
176    fn write_to(&self, writer: &mut TevWriter) {
177        writer.write(PacketType::OpenImageV2);
178        writer.write(self.grab_focus);
179        writer.write(self.image_name);
180        writer.write(self.channel_selector);
181    }
182}
183
184/// Reload an existing image with name or path `image_name` from disk.
185#[derive(Debug)]
186pub struct PacketReloadImage<'a> {
187    pub image_name: &'a str,
188    pub grab_focus: bool,
189}
190
191impl TevPacket for PacketReloadImage<'_> {
192    fn write_to(&self, writer: &mut TevWriter) {
193        writer.write(PacketType::ReloadImage);
194        writer.write(self.grab_focus);
195        writer.write(self.image_name);
196    }
197}
198
199/// Update part of an existing image with new pixel data.
200#[derive(Debug)]
201pub struct PacketUpdateImage<'a, S: AsRef<str> + 'a> {
202    pub image_name: &'a str,
203    pub grab_focus: bool,
204    pub channel_names: &'a [S],
205    pub channel_offsets: &'a [u64],
206    pub channel_strides: &'a [u64],
207    pub x: u32,
208    pub y: u32,
209    pub width: u32,
210    pub height: u32,
211    pub data: &'a [f32],
212}
213
214impl<'a, S: AsRef<str> + 'a> TevPacket for PacketUpdateImage<'a, S> {
215    fn write_to(&self, writer: &mut TevWriter) {
216        let channel_count = self.channel_names.len();
217
218        assert_ne!(channel_count, 0, "Must update at least one channel");
219        assert_eq!(channel_count, self.channel_offsets.len(), "Channel count must be consistent");
220        assert_eq!(channel_count, self.channel_strides.len(), "Channel count must be consistent");
221
222        let pixel_count = (self.width as u64) * (self.height as u64);
223        assert_ne!(pixel_count, 0, "Must update at least one pixel");
224
225        let max_data_index_used = self.channel_offsets.iter().zip(self.channel_strides)
226            .map(|(&o, &s)| o + (pixel_count - 1) * s)
227            .max().unwrap();
228        assert_eq!(max_data_index_used + 1, self.data.len() as u64, "Data size does not match actually used data range");
229
230        writer.write(PacketType::UpdateImageV3);
231        writer.write(self.grab_focus);
232        writer.write(self.image_name);
233        writer.write(channel_count as u32);
234        writer.write_all(self.channel_names.iter().map(AsRef::as_ref));
235        writer.write(self.x);
236        writer.write(self.y);
237        writer.write(self.width);
238        writer.write(self.height);
239        writer.write_all(self.channel_offsets);
240        writer.write_all(self.channel_strides);
241
242        writer.write_all(self.data)
243    }
244}
245
246/// Close an image.
247#[derive(Debug)]
248pub struct PacketCloseImage<'a> {
249    pub image_name: &'a str,
250}
251
252impl TevPacket for PacketCloseImage<'_> {
253    fn write_to(&self, writer: &mut TevWriter) {
254        writer.write(PacketType::CloseImage);
255        writer.write(self.image_name);
256    }
257}
258
259/// Create a new image with name `image_name`, size (`width`, `height`) and channels `channel_names`.
260#[derive(Debug)]
261pub struct PacketCreateImage<'a, S: AsRef<str> + 'a> {
262    pub image_name: &'a str,
263    pub grab_focus: bool,
264    pub width: u32,
265    pub height: u32,
266    pub channel_names: &'a [S],
267}
268
269impl<'a, S: AsRef<str> + 'a> TevPacket for PacketCreateImage<'a, S> {
270    fn write_to(&self, writer: &mut TevWriter) {
271        writer.write(PacketType::CreateImage);
272        writer.write(self.grab_focus);
273        writer.write(self.image_name);
274        writer.write(self.width);
275        writer.write(self.height);
276        writer.write(self.channel_names.len() as u32);
277        writer.write_all(self.channel_names.iter().map(AsRef::as_ref));
278    }
279}
280
281/// A buffer used to construct TCP packets. For internal use only.
282#[doc(hidden)]
283pub struct TevWriter {
284    target: Vec<u8>,
285}
286
287#[repr(C)]
288#[derive(Debug, Copy, Clone)]
289enum PacketType {
290    ReloadImage = 1,
291    CloseImage = 2,
292    CreateImage = 4,
293    UpdateImageV3 = 6,
294    OpenImageV2 = 7,
295}
296
297impl TevWriter {
298    fn write(&mut self, value: impl TevWritable) {
299        value.write_to(self);
300    }
301
302    fn write_all(&mut self, values: impl IntoIterator<Item=impl TevWritable>) {
303        for value in values {
304            value.write_to(self);
305        }
306    }
307}
308
309/// The trait implemented by all packets.
310#[doc(hidden)]
311pub trait TevPacket {
312    fn write_to(&self, writer: &mut TevWriter);
313}
314
315trait TevWritable {
316    fn write_to(self, writer: &mut TevWriter);
317}
318
319impl<T: TevWritable + Copy> TevWritable for &T {
320    fn write_to(self, writer: &mut TevWriter) {
321        (*self).write_to(writer);
322    }
323}
324
325impl TevWritable for bool {
326    fn write_to(self, writer: &mut TevWriter) {
327        writer.target.push(self as u8);
328    }
329}
330
331impl TevWritable for PacketType {
332    fn write_to(self, writer: &mut TevWriter) {
333        writer.target.push(self as u8);
334    }
335}
336
337impl TevWritable for u32 {
338    fn write_to(self, writer: &mut TevWriter) {
339        writer.target.extend_from_slice(&self.to_le_bytes());
340    }
341}
342
343impl TevWritable for u64 {
344    fn write_to(self, writer: &mut TevWriter) {
345        writer.target.extend_from_slice(&self.to_le_bytes());
346    }
347}
348
349impl TevWritable for f32 {
350    fn write_to(self, writer: &mut TevWriter) {
351        writer.target.extend_from_slice(&self.to_le_bytes());
352    }
353}
354
355impl TevWritable for &'_ str {
356    fn write_to(self, writer: &mut TevWriter) {
357        assert!(!self.contains('\0'), "cannot send strings containing '\\0'");
358        writer.target.extend_from_slice(self.as_bytes());
359        writer.target.push(0);
360    }
361}
362
363impl From<std::io::Error> for TevError {
364    fn from(io: std::io::Error) -> Self {
365        TevError::IO { io }
366    }
367}
368
369impl Display for TevError {
370    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
371        match self {
372            TevError::Command { io } =>
373                write!(f, "error during command execution: {}", io),
374            TevError::Stdout { io } =>
375                write!(f, "error during stdout reading: {}", io),
376            TevError::NoSocketResponse { read } =>
377                write!(f, "stdout did not contain socket, got '{}'", read),
378            TevError::TcpConnect { host, io } =>
379                write!(f, "error during attempted tcp connection to '{}': {}", host, io),
380            TevError::IO { io } =>
381                write!(f, "generic IO error: {}", io),
382        }
383    }
384}
385
386impl std::error::Error for TevError {
387    fn source(&self) -> Option<&(dyn Error + 'static)> {
388        match self {
389            TevError::Command { io } | TevError::Stdout { io } |
390            TevError::TcpConnect { host: _, io } | TevError::IO { io } =>
391                Some(io),
392            TevError::NoSocketResponse { read: _ } =>
393                None,
394        }
395    }
396}