1use crate::state::OriginState;
7use origin_app::AppInfo;
8use origin_connector::ConnectorDescriptor;
9use origin_domain::{Account, AccountId, AppError, ErrorContract, Health, Job, JobId};
10use origin_sync::{SyncStatus, SyncTarget};
11use serde::Serialize;
12use tauri::{AppHandle, State};
13use time::format_description::well_known::Rfc3339;
14
15#[derive(Debug, Serialize)]
20#[serde(transparent)]
21pub struct CommandError(ErrorContract);
22
23impl From<AppError> for CommandError {
24 fn from(error: AppError) -> Self {
25 tracing::warn!(kind = ?error.kind(), %error, "command failed");
26 Self(error.to_contract())
27 }
28}
29
30type CommandResult<T> = Result<T, CommandError>;
31
32#[tauri::command]
33pub async fn origin_app_info(
34 app: AppHandle,
35 state: State<'_, OriginState>,
36) -> CommandResult<AppInfo> {
37 let package = app.package_info();
38 Ok(AppInfo {
39 id: state.config().app_id.clone(),
40 name: package.name.clone(),
41 version: package.version.to_string(),
42 modules: state
43 .application()
44 .modules()
45 .iter()
46 .map(|module| (*module).to_owned())
47 .collect(),
48 })
49}
50
51#[tauri::command]
52pub async fn origin_setting_get(
53 state: State<'_, OriginState>,
54 key: String,
55) -> CommandResult<Option<serde_json::Value>> {
56 Ok(state
57 .application()
58 .platform()
59 .settings
60 .get_json(&key)
61 .await?)
62}
63
64#[tauri::command]
65pub async fn origin_setting_set(
66 state: State<'_, OriginState>,
67 key: String,
68 value: serde_json::Value,
69) -> CommandResult<()> {
70 state
71 .application()
72 .platform()
73 .settings
74 .set_json(&key, &value)
75 .await?;
76 Ok(())
77}
78
79#[tauri::command]
80pub async fn origin_settings_customised(
81 state: State<'_, OriginState>,
82) -> CommandResult<Vec<String>> {
83 Ok(state
84 .application()
85 .platform()
86 .settings
87 .customised_keys()
88 .await?)
89}
90
91#[tauri::command]
98pub async fn origin_open_url(state: State<'_, OriginState>, url: String) -> CommandResult<()> {
99 let application = state.application();
100 let opener = application.platform().opener.as_ref().ok_or_else(|| {
101 AppError::Permission("this application cannot open external urls".to_owned())
102 })?;
103
104 opener.open_url(&url).await?;
105 Ok(())
106}
107
108#[tauri::command]
110pub async fn origin_accounts(state: State<'_, OriginState>) -> CommandResult<Vec<Account>> {
111 Ok(state.application().platform().accounts.list().await?)
112}
113
114#[tauri::command]
119pub async fn origin_account_disconnect(
120 state: State<'_, OriginState>,
121 account: String,
122) -> CommandResult<()> {
123 state
124 .application()
125 .platform()
126 .accounts
127 .disconnect(&AccountId::new(account))
128 .await?;
129 Ok(())
130}
131
132#[tauri::command]
137pub async fn origin_connectors(
138 state: State<'_, OriginState>,
139) -> CommandResult<Vec<ConnectorDescriptor>> {
140 Ok(state
141 .application()
142 .platform()
143 .connectors
144 .iter()
145 .map(|connector| connector.descriptor())
146 .collect())
147}
148
149#[tauri::command]
151pub async fn origin_jobs(state: State<'_, OriginState>) -> CommandResult<Vec<Job>> {
152 Ok(state.application().platform().jobs.list().await)
153}
154
155#[tauri::command]
160pub async fn origin_job_cancel(state: State<'_, OriginState>, job: String) -> CommandResult<()> {
161 state
162 .application()
163 .platform()
164 .jobs
165 .cancel(&JobId::new(job))
166 .await?;
167 Ok(())
168}
169
170#[tauri::command]
171pub async fn origin_sync_status(state: State<'_, OriginState>) -> CommandResult<Vec<SyncStatus>> {
172 let application = state.application();
173 let engine = &application.platform().sync;
174 let now = application.platform().clock.now();
175
176 let mut statuses = Vec::new();
177 for target in engine.targets() {
178 let sync_state = engine.state(&target).await?;
179 let due_at = engine.due_at(&target).await.ok();
180
181 statuses.push(SyncStatus {
182 health: origin_sync::health_of(
183 &sync_state,
184 &engine.policy(&target).unwrap_or_default(),
185 now,
186 ),
187 state: sync_state,
188 due_at: due_at.and_then(|at| at.format(&Rfc3339).ok()),
189 target,
190 });
191 }
192
193 Ok(statuses)
194}
195
196#[tauri::command]
201pub async fn origin_sync_now(
202 state: State<'_, OriginState>,
203 target: SyncTarget,
204) -> CommandResult<()> {
205 state
206 .application()
207 .platform()
208 .sync
209 .sync_now(&target)
210 .await?;
211 Ok(())
212}
213
214#[tauri::command]
216pub async fn origin_health(state: State<'_, OriginState>) -> CommandResult<Health> {
217 Ok(state.application().platform().sync.health().await)
218}
219
220#[cfg(all(test, not(windows)))]
236mod tests {
237 use super::*;
238 use crate::HostConfig;
239 use crate::state::OriginState;
240 use origin_app::ApplicationBuilder;
241 use tauri::Manager;
242 use tokio_util::sync::CancellationToken;
243
244 fn mock_app_with_state() -> tauri::App<tauri::test::MockRuntime> {
245 let application = ApplicationBuilder::in_memory()
246 .build()
247 .expect("an in-memory application always builds");
248
249 let app = tauri::test::mock_app();
250 app.manage(OriginState::new(
251 application,
252 HostConfig::new("dev.origin.tests"),
253 CancellationToken::new(),
254 ));
255 app
256 }
257
258 #[tokio::test]
259 async fn health_reads_through_the_managed_state_to_the_sync_engine() {
260 let app = mock_app_with_state();
261
262 let health = origin_health(app.state()).await.unwrap();
263
264 assert_eq!(health, Health::Unknown, "no sync targets are registered");
265 }
266
267 #[tokio::test]
268 async fn a_setting_written_through_one_command_is_read_back_through_another() {
269 let app = mock_app_with_state();
270
271 origin_setting_set(app.state(), "theme".to_owned(), serde_json::json!("dark"))
272 .await
273 .unwrap();
274
275 let value = origin_setting_get(app.state(), "theme".to_owned())
276 .await
277 .unwrap();
278
279 assert_eq!(value, Some(serde_json::json!("dark")));
280 }
281
282 #[tokio::test]
283 async fn a_freshly_built_application_has_no_accounts_or_jobs() {
284 let app = mock_app_with_state();
285
286 assert!(origin_accounts(app.state()).await.unwrap().is_empty());
287 assert!(origin_jobs(app.state()).await.unwrap().is_empty());
288 }
289}