covey_plugin/
list.rs

1use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
2
3use crate::Menu;
4
5pub struct List {
6    pub items: Vec<ListItem>,
7    /// The kind of list to show.
8    ///
9    /// If this is [`None`], the list style will be the default set by
10    /// the user. Plugins should only set one if the content makes the most
11    /// sense with one of these styles.
12    pub style: Option<ListStyle>,
13    _priv: (),
14}
15
16impl List {
17    pub fn new(items: Vec<ListItem>) -> Self {
18        Self {
19            items,
20            style: None,
21            _priv: (),
22        }
23    }
24
25    #[must_use = "builder method consumes self"]
26    pub fn as_grid_with_columns(mut self, columns: u32) -> Self {
27        self.style = Some(ListStyle::GridWithColumns(columns));
28        self
29    }
30
31    #[must_use = "builder method consumes self"]
32    pub fn as_grid(mut self) -> Self {
33        self.style = Some(ListStyle::Grid);
34        self
35    }
36
37    #[must_use = "builder method consumes self"]
38    pub fn as_rows(mut self) -> Self {
39        self.style = Some(ListStyle::Rows);
40        self
41    }
42}
43
44#[non_exhaustive]
45pub enum ListStyle {
46    Rows,
47    Grid,
48    GridWithColumns(u32),
49}
50
51impl ListStyle {
52    pub(crate) fn into_proto(self) -> covey_proto::query_response::ListStyle {
53        use covey_proto::query_response::ListStyle as Proto;
54        match self {
55            ListStyle::Rows => Proto::Rows(()),
56            ListStyle::Grid => Proto::Grid(()),
57            ListStyle::GridWithColumns(columns) => Proto::GridWithColumns(columns),
58        }
59    }
60}
61
62// This should only be converted into a covey_proto::ListItem via the ListItemStore.
63#[derive(Clone)]
64pub struct ListItem {
65    pub title: String,
66    pub description: String,
67    pub icon: Option<Icon>,
68    /// Key is the command's ID.
69    pub(crate) commands: ListItemCallbacks,
70}
71
72impl ListItem {
73    pub fn new(title: impl Into<String>) -> Self {
74        let title = title.into();
75        Self {
76            title: title.clone(),
77            icon: None,
78            description: String::new(),
79            commands: ListItemCallbacks::new(title),
80        }
81    }
82
83    #[must_use = "builder method consumes self"]
84    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
85        self.description = desc.into();
86        self
87    }
88
89    #[must_use = "builder method consumes self"]
90    pub fn with_icon(mut self, icon: Option<Icon>) -> Self {
91        self.icon = icon;
92        self
93    }
94
95    #[must_use = "builder method consumes self"]
96    pub fn with_icon_name(mut self, name: impl Into<String>) -> Self {
97        self.icon = Some(Icon::Name(name.into()));
98        self
99    }
100
101    #[must_use = "builder method consumes self"]
102    pub fn with_icon_text(mut self, text: impl Into<String>) -> Self {
103        self.icon = Some(Icon::Text(text.into()));
104        self
105    }
106
107    /// Adds a command that can be called.
108    ///
109    /// This should not be used directly, use the extension trait generated
110    /// by [`crate::include_manifest!`] instead.
111    #[doc(hidden)]
112    #[must_use]
113    pub fn add_command(mut self, name: &'static str, callback: ActivationFunction) -> Self {
114        self.commands.add_command(name, callback);
115        self
116    }
117}
118
119#[derive(Debug, Clone)]
120pub enum Icon {
121    Name(String),
122    Text(String),
123}
124
125impl Icon {
126    pub(crate) fn into_proto(self) -> covey_proto::list_item::Icon {
127        use covey_proto::list_item::Icon as Proto;
128        match self {
129            Self::Name(name) => Proto::Name(name),
130            Self::Text(text) => Proto::Text(text),
131        }
132    }
133}
134
135// TODO: figure out the bounds to put here and on the generated extension trait
136// methods (covey-schema/src/generate/generate_ext.rs#generate_ext_trait).
137//
138// a tonic server requires that all the functions are Send + Sync.
139// this means that all futures called must be Send + Sync.
140//
141// the type of the callback basically has two options:
142// 1) impl AsyncFn() -> T + Send + Sync
143// 2) impl Fn() -> Fut + Send + Sync where Fut: Future<Output = T> + Send + Sync
144//
145// with 1) the returned future can borrow from variables captured by the closure.
146// however, there's no way to write the Send + Sync bound on the future itself.
147//
148// with 2) the returned future requires Send + Sync but cannot borrow from
149// variables captured by the closure.
150//
151// the best option would be once return-type notation is stabilised, to annotate
152// the future of AsyncFn as requiring Send + Sync.
153//
154// Alternatively, use `tokio::task::spawn_local` so that the future does not
155// need to be Send + Sync. This makes 1) work fine, but means that the server
156// cannot be multi-threaded. This is probably fine, as local async executors
157// are easier to work with (https://maciej.codes/2022-06-09-local-async.html).
158
159type DynFuture<T> = Pin<Box<dyn Future<Output = T>>>;
160type ActivationFunction = Arc<dyn Fn(Menu) -> DynFuture<()> + Send + Sync>;
161
162#[derive(Clone)]
163pub(crate) struct ListItemCallbacks {
164    /// Key is the command's ID.
165    commands: HashMap<&'static str, ActivationFunction>,
166    item_title: String,
167}
168
169impl ListItemCallbacks {
170    pub(crate) fn new(title: String) -> Self {
171        Self {
172            commands: HashMap::default(),
173            item_title: title,
174        }
175    }
176
177    pub(crate) fn add_command(&mut self, name: &'static str, callback: ActivationFunction) {
178        self.commands.insert(name, callback);
179    }
180
181    /// Calls a command by name, doing nothing if the command is not found.
182    pub(crate) async fn call_command(&self, name: &str, menu: Menu) {
183        if let Some(cmd) = self.commands.get(name) {
184            crate::rank::register_usage(&self.item_title);
185            cmd(menu).await;
186        }
187    }
188
189    pub(crate) fn ids(&self) -> impl Iterator<Item = &'static str> + use<'_> {
190        self.commands.keys().copied()
191    }
192}