Skip to main content

tilo_app/framework/
reference.rs

1//! Opening a reference from any app.
2//!
3//! A [`ResourceRef`] names something another app keeps: a chat message's
4//! page, an issue's link, what an agent was asked about. Opening it
5//! is one action, the resource's `open` in its own app
6//! ([`ResourceRef::open_target`]), sent through the session's action door
7//! as the person who asked: the host launches the app on the record when no
8//! window shows it and admits the open under that person's grants. A chip
9//! ([`ViewCx::reference`](super::ViewCx::reference), or [`LEAD`] on a
10//! painted [`ChipFace`](ui_core::widget::ChipFace)) is how a reference shows.
11
12use std::cell::RefCell;
13use std::rc::Rc;
14
15use tilo_actions::InvocationUpdate;
16use tilo_actions::remote::{ActionHostError, InvokeArgument};
17use tilo_reference::ResourceRef;
18use ui_core::widget::ChipLead;
19use ui_core::{Arrival, InvocationCall, RemoteActions};
20
21/// Ask the host to open what `href`, a `tilo://open#…` address, names: the
22/// app that keeps that resource shows it, launched first when no window
23/// shows it. An address that names no app of this desktop is refused.
24pub(crate) fn open_link(opener: &Opener, href: &str) -> Result<(), String> {
25    let link = tilo_reference::link::Link::parse_native(href).map_err(|error| error.to_string())?;
26    let address = link
27        .target
28        .resource
29        .ok_or_else(|| "The link names no resource".to_owned())?;
30    let app = match &link.target.record {
31        Some(_) => link.target.app.as_ref().map(|app| app.as_str().to_owned()),
32        None => ui_core::app_surface::app_of_kind(&address.kind),
33    }
34    .ok_or_else(|| "No app of this desktop keeps what the link names".to_owned())?;
35    // `-` is the app's own record of the workspace; any other id names it.
36    let record = link
37        .target
38        .record
39        .as_ref()
40        .map(tilo_reference::link::RecordSegment::provider_part)
41        .filter(|id| !id.is_empty());
42    opener.open(&app, record, &address.into_ref(String::new()))
43}
44
45/// Another resource's published action, as an effect calls it
46/// ([`EffectCx::call`](super::EffectCx::call)): the resource a reference
47/// names, in the app that keeps it, and the action's arguments. The host
48/// decides it as the effect's caller, as it decides every action.
49#[derive(Clone, Debug, PartialEq)]
50pub struct Call {
51    app: String,
52    record: Option<String>,
53    reference: ResourceRef,
54    action: String,
55    arguments: Result<serde_json::Map<String, serde_json::Value>, String>,
56    /// The host provider that answers it, for an action the host itself
57    /// publishes ([`Self::host`]) rather than an app.
58    host: Option<String>,
59}
60
61impl Call {
62    /// `action` of the resource `reference` names, which `app` keeps in
63    /// `record` (`None`: the app's own record of the workspace), with no
64    /// arguments yet.
65    #[must_use]
66    pub fn on(app: &str, record: Option<&str>, reference: &ResourceRef, action: &str) -> Self {
67        Self {
68            app: app.to_owned(),
69            record: record.map(str::to_owned),
70            reference: reference.clone(),
71            action: action.to_owned(),
72            arguments: Ok(serde_json::Map::new()),
73            host: None,
74        }
75    }
76
77    /// `action` of `resource`, which the host publishes under `provider`
78    /// itself (the desktop's `open` on `desktop:session`, under
79    /// `tilo.desktop`), with no arguments yet.
80    #[must_use]
81    pub fn host(provider: &str, resource: &str, action: &str) -> Self {
82        Self {
83            app: String::new(),
84            record: None,
85            reference: ResourceRef::whole(resource, resource, String::new()),
86            action: action.to_owned(),
87            arguments: Ok(serde_json::Map::new()),
88            host: Some(provider.to_owned()),
89        }
90    }
91
92    /// The call with `arguments`, a struct or map of the action's
93    /// parameters by name.
94    #[must_use]
95    pub fn with(mut self, arguments: impl serde::Serialize) -> Self {
96        self.arguments = match serde_json::to_value(arguments) {
97            Ok(serde_json::Value::Object(arguments)) => Ok(arguments),
98            Ok(_) => Err("an action's arguments are named".to_owned()),
99            Err(error) => Err(error.to_string()),
100        };
101        self
102    }
103
104    /// The action this calls.
105    #[must_use]
106    pub fn action(&self) -> &str {
107        &self.action
108    }
109
110    /// Where the call goes in this window's workspace, and what it carries.
111    pub(crate) fn request(
112        &self,
113    ) -> Result<(tilo_actions::remote::RemoteActionTarget, InvokeArgument), String> {
114        let arguments = self.arguments.clone()?;
115        let target = match &self.host {
116            Some(provider) => tilo_actions::remote::RemoteActionTarget {
117                provider: provider.clone(),
118                address: tilo_actions::ActionAddress::new(
119                    self.reference.id.clone(),
120                    self.action.clone(),
121                ),
122            },
123            None => self.reference.action_target(
124                &workspace()?,
125                &self.app,
126                self.record.as_deref(),
127                &self.action,
128            ),
129        };
130        Ok((target, InvokeArgument::Json(arguments)))
131    }
132}
133
134/// The action a refused open is told as.
135pub(crate) const OPEN: &str = tilo_actions::remote::APPLICATION_OPEN_ACTION;
136
137/// What leads a reference's label on its chip.
138pub const LEAD: ChipLead = ui_core::widget::REFERENCE_LEAD;
139
140/// The workspace this window is in, or why nothing opens from it.
141pub fn workspace() -> Result<String, String> {
142    // A window hosted among others in one process (the browser) is in the
143    // workspace its host put it in, not the process's.
144    if let Some(workspace) = ui_core::window_toolbar::workspace() {
145        return Ok(workspace);
146    }
147    let workspace = ui_core::collaboration::local_cluster_id();
148    if workspace.is_nil() {
149        Err("This window is in no workspace".into())
150    } else {
151        Ok(workspace.to_string())
152    }
153}
154
155/// A view's opens, and the actions of its own record it sends through the
156/// host ([`Dispatcher::through_host`](super::Dispatcher::through_host)), on
157/// their own client of the session's action door, which the window's root
158/// carries to the host.
159#[derive(Clone, Default)]
160pub(crate) struct Opener(Rc<RefCell<Opening>>);
161
162#[derive(Default)]
163struct Opening {
164    door: Option<RemoteActions>,
165    /// Each call, by the action it invokes.
166    calls: Vec<(String, InvocationCall)>,
167    /// The reads views follow ([`super::query::Query`]), each kept while
168    /// the scope that asked for it lives.
169    reads: Vec<(std::rc::Weak<()>, Rc<dyn std::any::Any>)>,
170}
171
172impl Opener {
173    /// Ask the host to open `reference`, which `app` keeps in `record`.
174    pub(crate) fn open(
175        &self,
176        app: &str,
177        record: Option<&str>,
178        reference: &ResourceRef,
179    ) -> Result<(), String> {
180        let target = reference.open_target(&workspace()?, app, record);
181        self.invoke(target, InvokeArgument::None);
182        Ok(())
183    }
184
185    /// Ask the host to invoke `address`, an action of the window's own
186    /// record, with `arguments`, as the person using it.
187    pub(crate) fn invoke_own(
188        &self,
189        address: tilo_actions::ActionAddress,
190        arguments: serde_json::Map<String, serde_json::Value>,
191    ) {
192        self.invoke(
193            tilo_actions::remote::RemoteActionTarget {
194                provider: tilo_actions::remote::OWN_PROVIDER.into(),
195                address,
196            },
197            InvokeArgument::Json(arguments),
198        );
199    }
200
201    pub(crate) fn invoke(
202        &self,
203        target: tilo_actions::remote::RemoteActionTarget,
204        argument: InvokeArgument,
205    ) {
206        let action = target.address.action.clone();
207        let mut opening = self.0.borrow_mut();
208        let call = opening
209            .door
210            .get_or_insert_with(RemoteActions::current)
211            .invoke(target, argument, None);
212        opening.calls.push((action, call));
213    }
214
215    /// Ask the host to invoke `target` with `argument` as a request of its
216    /// own (never told as this view's refusal), continuing `parent`, the
217    /// invocation the host delivered here for someone: the host decides it
218    /// as that invocation's caller. Without one it is the app's own request.
219    pub(crate) fn call(
220        &self,
221        parent: Option<tilo_actions::InvocationId>,
222        target: tilo_actions::remote::RemoteActionTarget,
223        argument: InvokeArgument,
224    ) -> InvocationCall {
225        self.0
226            .borrow_mut()
227            .door
228            .get_or_insert_with(RemoteActions::current)
229            .invoke_continuing(
230                parent,
231                tilo_actions::InvocationId::new(),
232                target,
233                argument,
234                None,
235            )
236    }
237
238    /// Follow the result of the action at `target` on this view's door,
239    /// telling `then` each update ([`RemoteActions::follow_with`]).
240    pub(crate) fn follow(
241        &self,
242        target: tilo_actions::remote::RemoteActionTarget,
243        argument: InvokeArgument,
244        then: impl FnMut(&Arrival<InvocationUpdate, ActionHostError>) + Send + 'static,
245    ) -> ui_core::Followed {
246        self.0
247            .borrow_mut()
248            .door
249            .get_or_insert_with(RemoteActions::current)
250            .follow_with(target, argument, then)
251    }
252
253    /// Keep `read` while `alive` lives.
254    pub(crate) fn keep(&self, alive: std::rc::Weak<()>, read: Rc<dyn std::any::Any>) {
255        self.0.borrow_mut().reads.push((alive, read));
256    }
257
258    /// Why each call that ended since the last call did not go ahead, by
259    /// the action it invoked.
260    pub(crate) fn take_refusals(&self) -> Vec<(String, String)> {
261        let mut refusals = Vec::new();
262        self.0.borrow_mut().calls.retain(|(action, call)| {
263            call.with(|call| match call {
264                Arrival::Pending { .. }
265                | Arrival::Ready(
266                    InvocationUpdate::Accepted { .. }
267                    | InvocationUpdate::Progress { .. }
268                    | InvocationUpdate::Item { .. },
269                ) => true,
270                Arrival::Ready(InvocationUpdate::Failed { error, .. }) => {
271                    refusals.push((action.clone(), error.message.clone()));
272                    false
273                }
274                Arrival::Failed(error) => {
275                    refusals.push((action.clone(), error.to_string()));
276                    false
277                }
278                Arrival::Ready(_) => false,
279            })
280        });
281        refusals
282    }
283
284    /// The client, once anything was opened: the root carries its requests
285    /// and answers.
286    pub(crate) fn door(&self) -> Option<RemoteActions> {
287        // A read whose scope is gone stops here, before the door carries
288        // its cancellation.
289        let gone: Vec<_> = {
290            let mut opening = self.0.borrow_mut();
291            let (kept, gone) = std::mem::take(&mut opening.reads)
292                .into_iter()
293                .partition(|(alive, _)| alive.strong_count() > 0);
294            opening.reads = kept;
295            gone
296        };
297        drop(gone);
298        self.0.borrow().door.clone()
299    }
300}