Skip to main content

tocat_api/
channel.rs

1//! Side channels: the only way a plugin reaches the outside world.
2//!
3//! A plugin never opens a file. At build time it describes the sink it wants
4//! and receives an opaque [`ChannelId`]. At run time it queues writes against
5//! that id. The host owns the file descriptor, dedupes identical targets so two
6//! plugins pointing at the same path share one buffered writer, and can refuse
7//! a target outright (see stdout, below).
8
9use std::path::PathBuf;
10
11use serde::{Deserialize, Serialize};
12
13use crate::error::Result;
14
15/// Opaque handle to a host-owned side channel.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
17pub struct ChannelId(pub u32);
18
19impl ChannelId {
20    #[must_use]
21    pub fn index(self) -> usize {
22        self.0 as usize
23    }
24}
25
26/// Where a side channel points.
27///
28/// Note the absence of stdout: on a `-` / stdio endpoint that stream carries
29/// relay payload, and interleaving dump output into it would corrupt the
30/// transfer. Hosts are expected to reject it.
31#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(tag = "type", rename_all = "kebab-case")]
33pub enum ChannelTarget {
34    Stderr,
35    File {
36        path: PathBuf,
37        #[serde(default)]
38        append: bool,
39    },
40}
41
42impl ChannelTarget {
43    /// Create a File ChannelTarget from something path-like
44    pub fn file(path: impl Into<PathBuf>) -> Self {
45        Self::File {
46            path: path.into(),
47            append: true,
48        }
49    }
50}
51
52/// Implemented by the host. Handed to plugins during construction only.
53pub trait HostBuilder {
54    /// Reserve a side channel, returning a handle to write to later.
55    ///
56    /// Implementations should return the same [`ChannelId`] for equal targets.
57    fn open_channel(&mut self, target: ChannelTarget) -> Result<ChannelId>;
58}