1mod camera;
2pub mod cli;
3mod config;
4mod diagnostics;
5mod dnd;
6mod hooks;
7mod ipc;
8mod pause_store;
9mod platform;
10mod renderer_log;
11mod scheduler;
12mod screen_time_store;
13mod secure_io;
14mod stats;
15mod supporter;
16#[cfg(test)]
17mod test_support;
18mod tray;
19mod updater;
20mod video;
21
22use scheduler::Scheduler;
23use tauri::{Manager, WindowEvent};
24use tauri_plugin_autostart::MacosLauncher;
25use tauri_plugin_log::{Target, TargetKind};
26
27pub fn should_hide_on_close(window_label: &str) -> bool {
28 window_label == "main"
29}
30
31#[cfg_attr(mobile, tauri::mobile_entry_point)]
32pub fn run() {
33 let log_level = if cfg!(debug_assertions) {
34 log::LevelFilter::Debug
35 } else {
36 log::LevelFilter::Info
37 };
38
39 let mut log_targets = vec![Target::new(TargetKind::LogDir {
40 file_name: Some("entracte".to_string()),
41 })];
42 if cfg!(debug_assertions) {
43 log_targets.push(Target::new(TargetKind::Stdout));
44 log_targets.push(Target::new(TargetKind::Stderr));
45 }
46
47 let logger = tauri_plugin_log::Builder::new()
48 .targets(log_targets)
49 .level(log_level)
50 .max_file_size(1024 * 1024)
51 .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepSome(5))
52 .build();
53
54 tauri::Builder::default()
55 .on_window_event(|window, event| {
56 if let WindowEvent::CloseRequested { api, .. } = event {
57 if should_hide_on_close(window.label()) {
58 api.prevent_close();
59 let _ = window.hide();
60 }
61 }
62 })
63 .plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| {
64 handle_cli_argv(app, argv);
65 }))
66 .plugin(logger)
67 .plugin(tauri_plugin_opener::init())
68 .plugin(tauri_plugin_dialog::init())
69 .plugin(tauri_plugin_notification::init())
70 .plugin(tauri_plugin_autostart::init(
71 MacosLauncher::LaunchAgent,
72 None,
73 ))
74 .invoke_handler(tauri::generate_handler![
75 scheduler::get_settings,
76 scheduler::update_settings,
77 scheduler::set_hooks,
78 scheduler::pause,
79 scheduler::resume,
80 scheduler::get_pause_info,
81 scheduler::end_break,
82 scheduler::trigger_test_break,
83 scheduler::postpone_break,
84 scheduler::skip_next_break,
85 scheduler::get_postpone_state,
86 scheduler::get_last_break_info,
87 scheduler::resume_last_break,
88 scheduler::get_break_stats,
89 scheduler::get_current_break,
90 scheduler::reset_break_stats,
91 scheduler::get_stats_digest,
92 scheduler::export_stats_csv,
93 scheduler::clear_event_log,
94 scheduler::get_idle_secs,
95 scheduler::get_screen_time,
96 scheduler::list_profiles,
97 scheduler::get_active_profile,
98 scheduler::set_active_profile,
99 scheduler::create_profile,
100 scheduler::duplicate_profile,
101 scheduler::rename_profile,
102 scheduler::delete_profile,
103 scheduler::reorder_profiles,
104 scheduler::reset_profile_to_defaults,
105 updater::check_for_update,
106 diagnostics::build_diagnostics_report,
107 platform::get_platform,
108 renderer_log::report_renderer_error,
109 get_supporter_status,
110 verify_supporter_key,
111 remove_supporter,
112 ])
113 .setup(|app| {
114 #[cfg(target_os = "macos")]
115 app.set_activation_policy(tauri::ActivationPolicy::Accessory);
116
117 let config_dir = app
118 .path()
119 .app_config_dir()
120 .expect("app_config_dir resolves");
121 let _ = secure_io::ensure_user_only_dir(&config_dir);
122 let config_path = config_dir.join("settings.json");
123 let pause_path = config_dir.join("pause.json");
124 let data_dir = app
125 .path()
126 .app_data_dir()
127 .unwrap_or_else(|_| config_dir.clone());
128 let _ = secure_io::ensure_user_only_dir(&data_dir);
129 if let Ok(log_dir) = app.path().app_log_dir() {
130 let _ = secure_io::ensure_user_only_dir(&log_dir);
131 let _ = secure_io::tighten_existing_files_in_dir(&log_dir);
132 secure_io::spawn_periodic_dir_tighten(log_dir, std::time::Duration::from_secs(60));
137 }
138 let events_path = data_dir.join("events.jsonl");
139 let _ = secure_io::tighten_existing_file(&events_path);
140 let screen_time_path = data_dir.join("screen_time.json");
141
142 let scheduler = Scheduler::new(config_path, pause_path, events_path, screen_time_path);
143 scheduler.spawn(app.handle().clone());
144 app.manage(scheduler);
145
146 let supporter_path = supporter::file_path(&data_dir);
147 app.manage(SupporterAppState {
148 path: supporter_path.clone(),
149 client: reqwest::Client::new(),
150 });
151 spawn_supporter_revalidation(supporter_path);
152
153 tray::setup(app.handle())?;
154
155 if let Err(e) = ipc::start_server(app.handle().clone(), data_dir.clone()) {
156 log::warn!("ipc: failed to start server: {e}");
157 }
158 Ok(())
159 })
160 .run(tauri::generate_context!())
161 .expect("error while running tauri application");
162}
163
164fn handle_cli_argv(app: &tauri::AppHandle, argv: Vec<String>) {
165 if let Some(w) = app.get_webview_window("main") {
166 let _ = w.show();
167 let _ = w.set_focus();
168 }
169 if argv.len() > 1 {
170 log::debug!(
171 "cli: single-instance second invocation received args {:?} \
172 — these route via the local TCP IPC channel, not single-instance forwarding",
173 &argv[1..]
174 );
175 }
176}
177
178pub struct SupporterAppState {
179 pub path: std::path::PathBuf,
180 pub client: reqwest::Client,
181}
182
183fn spawn_supporter_revalidation(path: std::path::PathBuf) {
184 tauri::async_runtime::spawn(async move {
185 let client = reqwest::Client::new();
186 loop {
187 let Some(record) = supporter::load(&path) else {
188 tokio::time::sleep(std::time::Duration::from_secs(60 * 60 * 24)).await;
189 continue;
190 };
191 if supporter::needs_revalidation(record.last_validated_at, chrono::Utc::now()) {
192 match supporter::validate_remote(&client, &record.license_key, &record.instance_id)
193 .await
194 {
195 Ok(true) => {
196 let mut updated = record.clone();
197 updated.last_validated_at = chrono::Utc::now();
198 if let Err(e) = supporter::save(&path, &updated) {
199 log::warn!("supporter: failed to persist validation timestamp: {e}");
200 }
201 }
202 Ok(false) => {
203 log::warn!("supporter: license no longer valid, removing local record");
204 let _ = supporter::delete(&path);
205 }
206 Err(e) => {
207 log::warn!("supporter: validate request failed: {e}");
208 }
209 }
210 }
211 tokio::time::sleep(std::time::Duration::from_secs(60 * 60 * 24)).await;
212 }
213 });
214}
215
216#[tauri::command]
217async fn get_supporter_status(
218 state: tauri::State<'_, SupporterAppState>,
219) -> Result<supporter::SupporterStatus, String> {
220 let record = supporter::load(&state.path);
221 Ok(supporter::SupporterStatus::from_record(
222 record.as_ref(),
223 chrono::Utc::now(),
224 ))
225}
226
227#[tauri::command]
228async fn verify_supporter_key(
229 state: tauri::State<'_, SupporterAppState>,
230 license_key: String,
231) -> Result<supporter::SupporterStatus, String> {
232 let key = license_key.trim().to_string();
233 if key.is_empty() {
234 return Err("license key is empty".to_string());
235 }
236 let host = sysinfo::System::host_name().unwrap_or_else(|| "entracte-machine".to_string());
237 let instance_name = format!("entracte-{host}");
238 let instance_id = supporter::activate_remote(&state.client, &key, &instance_name).await?;
239 let now = chrono::Utc::now();
240 let record = supporter::SupporterRecord {
241 license_key: key,
242 instance_id,
243 activated_at: now,
244 last_validated_at: now,
245 };
246 supporter::save(&state.path, &record).map_err(|e| e.to_string())?;
247 Ok(supporter::SupporterStatus::from_record(Some(&record), now))
248}
249
250#[tauri::command]
251async fn remove_supporter(
252 state: tauri::State<'_, SupporterAppState>,
253) -> Result<supporter::SupporterStatus, String> {
254 supporter::delete(&state.path).map_err(|e| e.to_string())?;
255 Ok(supporter::SupporterStatus::from_record(
256 None,
257 chrono::Utc::now(),
258 ))
259}
260
261#[cfg(test)]
262mod tests {
263 use super::should_hide_on_close;
264
265 #[test]
266 fn main_window_hides_on_close() {
267 assert!(should_hide_on_close("main"));
268 }
269
270 #[test]
271 fn overlay_windows_destroy_on_close() {
272 assert!(!should_hide_on_close("overlay-0"));
273 assert!(!should_hide_on_close("overlay-1"));
274 assert!(!should_hide_on_close("overlay-7"));
275 }
276
277 #[test]
278 fn unknown_labels_destroy_on_close() {
279 assert!(!should_hide_on_close(""));
280 assert!(!should_hide_on_close("Main"));
281 assert!(!should_hide_on_close("main "));
282 assert!(!should_hide_on_close(" main"));
283 assert!(!should_hide_on_close("settings"));
284 assert!(!should_hide_on_close("preferences"));
285 }
286}