covey_plugin/
plugin.rs

1use std::future::Future;
2
3use tokio::sync::mpsc;
4use tokio_stream::wrappers::ReceiverStream;
5
6use crate::{List, Menu, Result, manifest::ManifestDeserialization, server::ServerState};
7
8pub trait Plugin: Sized + Send + Sync + 'static {
9    /// The user's configuration for this plugin.
10    ///
11    /// Use `()` if this plugin has no configuration.
12    type Config: ManifestDeserialization;
13
14    fn new(config: Self::Config) -> impl Future<Output = Result<Self>> + Send;
15
16    fn query(&self, query: String) -> impl Future<Output = Result<List>> + Send;
17}
18
19type TonicResult<T> = Result<tonic::Response<T>, tonic::Status>;
20
21#[tonic::async_trait]
22impl<T> covey_proto::plugin_server::Plugin for ServerState<T>
23where
24    T: Plugin,
25{
26    async fn initialise(
27        &self,
28        request: tonic::Request<covey_proto::InitialiseRequest>,
29    ) -> TonicResult<()> {
30        let request = request.into_inner();
31
32        let mut guard = self.plugin.write().await;
33
34        let config = ManifestDeserialization::try_from_input(&request.json)
35            .map_err(|e| tonic::Status::invalid_argument(e.to_string()))?;
36        let plugin = T::new(config).await.map_err(into_tonic_status)?;
37
38        *guard = Some(plugin);
39
40        Ok(tonic::Response::new(()))
41    }
42
43    async fn query(
44        &self,
45        request: tonic::Request<covey_proto::QueryRequest>,
46    ) -> TonicResult<covey_proto::QueryResponse> {
47        let list = self
48            .plugin
49            .read()
50            .await
51            .as_ref()
52            .expect("plugin has not been initialised")
53            .query(request.into_inner().query)
54            .await
55            .map_err(into_tonic_status)?;
56
57        Ok(tonic::Response::new(
58            self.list_item_store.lock().store_query_result(list),
59        ))
60    }
61
62    type ActivateStream = ReceiverStream<Result<covey_proto::ActivationResponse, tonic::Status>>;
63
64    async fn activate(
65        &self,
66        request: tonic::Request<covey_proto::ActivationRequest>,
67    ) -> TonicResult<Self::ActivateStream> {
68        let request = request.into_inner();
69        let id = request.selection_id;
70        let callbacks =
71            self.list_item_store
72                .lock()
73                .fetch_callbacks_of(id)
74                .ok_or(tonic::Status::data_loss(format!(
75                    "failed to fetch callback of list item with id {id}"
76                )))?;
77
78        let (tx, rx) = mpsc::channel(4);
79        let menu = Menu { sender: tx };
80
81        // tonic plugin requires methods to be Send + Sync, but this
82        // is annoying. spawn_local makes this future no longer require
83        // Send + Sync.
84        tokio::task::spawn_local(async move {
85            callbacks.call_command(&request.command_name, menu).await;
86        })
87        .await
88        // JoinHandle resolves to an err if the task panicked.
89        .map_err(|e| tonic::Status::internal(e.to_string()))?;
90
91        Ok(tonic::Response::new(ReceiverStream::new(rx)))
92    }
93}
94
95#[expect(
96    clippy::needless_pass_by_value,
97    reason = "easier to only use path when mapping"
98)]
99fn into_tonic_status(e: anyhow::Error) -> tonic::Status {
100    tonic::Status::unknown(
101        e.chain()
102            .map(ToString::to_string)
103            .collect::<Vec<_>>()
104            .join("\n"),
105    )
106}