1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
//! Update checking logic for the window manager.
//!
//! Handles periodic update checks, forced update checks (from UI),
//! desktop notifications, and syncing update state to settings windows.
use crate::update_checker::{UpdateCheckResult, UpdateInfo};
/// Show desktop notification when update is available.
pub(super) fn notify_update_available(info: &UpdateInfo) {
let version_str = info.version.strip_prefix('v').unwrap_or(&info.version);
let current = env!("CARGO_PKG_VERSION");
let summary = format!("par-term v{} Available", version_str);
let body = format!(
"You have v{}. Check Settings > Advanced > Updates.",
current
);
crate::platform::deliver_desktop_notification(
&summary,
&body,
8000,
crate::platform::NotificationUrgency::Normal,
);
}
/// Convert a main-crate UpdateCheckResult to the settings-ui crate's type.
pub(super) fn to_settings_update_result(
result: &UpdateCheckResult,
) -> crate::settings_ui::UpdateCheckResult {
match result {
UpdateCheckResult::UpToDate => crate::settings_ui::UpdateCheckResult::UpToDate,
UpdateCheckResult::UpdateAvailable(info) => {
crate::settings_ui::UpdateCheckResult::UpdateAvailable(
crate::settings_ui::UpdateCheckInfo {
version: info.version.clone(),
release_notes: info.release_notes.clone(),
release_url: info.release_url.clone(),
published_at: info.published_at.clone(),
},
)
}
UpdateCheckResult::Disabled => crate::settings_ui::UpdateCheckResult::Disabled,
UpdateCheckResult::Skipped => crate::settings_ui::UpdateCheckResult::Skipped,
UpdateCheckResult::Error(e) => crate::settings_ui::UpdateCheckResult::Error(e.clone()),
}
}
/// Extract the available version string from an update result (None if not available).
pub(super) fn update_available_version(result: &UpdateCheckResult) -> Option<String> {
match result {
UpdateCheckResult::UpdateAvailable(info) => Some(
info.version
.strip_prefix('v')
.unwrap_or(&info.version)
.to_string(),
),
_ => None,
}
}
use super::WindowManager;
/// Outcome of an off-thread update check, delivered to the main thread.
pub(crate) struct UpdateCheckOutcome {
pub result: UpdateCheckResult,
pub should_save: bool,
/// `true` for the user-initiated "Check Now" — skips the desktop notification
/// (and last-notified recording) that the periodic check sends on a
/// newly-discovered update, preserving the prior synchronous behavior.
pub force: bool,
}
impl WindowManager {
/// Check for updates (called periodically from about_to_wait)
///
/// The blocking HTTP fetch runs off the main thread via
/// `runtime.spawn_blocking`; results arrive through `update_check_rx` and
/// are applied on the main thread by the drain at the top of this method.
/// Previously the synchronous `ureq` GET (30s timeout to the GitHub releases
/// API) ran on the main event-loop thread, freezing all I/O whenever the
/// network was slow or the request hung.
pub fn check_for_updates(&mut self) {
use std::time::{Duration, Instant};
// Apply any off-thread checks that completed since the last frame.
// Logging, notifications, status sync, and config save all touch `self`
// and must run here, on the main thread.
while let Ok(outcome) = self.update_check_rx.try_recv() {
self.process_update_result(outcome);
}
let now = Instant::now();
// Schedule initial check shortly after startup (5 seconds delay)
if self.next_update_check.is_none() {
self.next_update_check = Some(now + Duration::from_secs(5));
return;
}
// Check if it's time for scheduled check
if let Some(next_check) = self.next_update_check
&& now >= next_check
{
// Offload the blocking check off the main event-loop thread.
// `load_full()` yields an owned `Arc<Config>` that is `Send`, unlike
// `load()`'s thread-bound `Guard`. Mirrors the DynamicProfileManager
// pattern of `spawn_blocking` for synchronous ureq fetches.
let checker = std::sync::Arc::clone(&self.update_checker);
let config = self.config.load_full();
let tx = self.update_check_tx.clone();
self.runtime.spawn_blocking(move || {
let (result, should_save) = checker.check_now(&config, false);
let _ = tx.send(UpdateCheckOutcome {
result,
should_save,
force: false,
});
});
// Schedule the next check immediately so the timer doesn't re-fire
// while one is in flight; `check_now`'s `check_in_progress` guard
// also prevents duplicate concurrent network fetches.
self.next_update_check = self
.config
.load()
.updates
.update_check_frequency
.as_seconds()
.map(|secs| now + Duration::from_secs(secs));
}
}
/// Apply a completed update-check result on the main thread: log, notify,
/// sync to status bar / update state, and persist the last-check timestamp.
fn process_update_result(&mut self, outcome: UpdateCheckOutcome) {
use crate::update_checker::current_timestamp;
let UpdateCheckOutcome {
result,
should_save,
force,
} = outcome;
let mut config_changed = should_save;
match &result {
UpdateCheckResult::UpdateAvailable(info) => {
let version_str = info
.version
.strip_prefix('v')
.unwrap_or(&info.version)
.to_string();
log::info!(
"Update available: {} (current: {})",
version_str,
env!("CARGO_PKG_VERSION")
);
// Only the periodic check desktop-notifies (and records the
// last-notified version); force checks just log, preserving
// the prior synchronous behavior.
if !force {
let already_notified = self
.config
.load()
.updates
.last_notified_version
.as_ref()
.is_some_and(|v| v == &version_str);
if !already_notified {
notify_update_available(info);
self.config.rcu(|old| {
let mut new = (**old).clone();
new.updates.last_notified_version = Some(version_str.clone());
std::sync::Arc::new(new)
});
config_changed = true;
}
}
}
UpdateCheckResult::UpToDate => {
log::info!("par-term is up to date ({})", env!("CARGO_PKG_VERSION"));
}
UpdateCheckResult::Error(e) => {
log::warn!("Update check failed: {}", e);
}
UpdateCheckResult::Disabled | UpdateCheckResult::Skipped => {
// Silent
}
}
self.last_update_result = Some(result);
// Sync update version to status bar widgets
let version = self
.last_update_result
.as_ref()
.and_then(update_available_version);
let result_clone = self.last_update_result.clone();
for ws in self.windows.values_mut() {
ws.status_bar_ui.update_available_version = version.clone();
ws.update_state.last_result = result_clone.clone();
}
// Sync to the settings window if open — covers both the periodic check
// and the user-triggered "Check Now", whose off-thread result lands here.
if let Some(settings_window) = &mut self.settings_window {
settings_window.settings_ui.last_update_result = self
.last_update_result
.as_ref()
.map(to_settings_update_result);
settings_window.request_redraw();
}
// Save config with updated timestamp if check was successful
if config_changed {
self.config.rcu(|old| {
let mut new = (**old).clone();
new.updates.last_update_check = Some(current_timestamp());
std::sync::Arc::new(new)
});
if let Err(e) = self.config.load().save() {
log::warn!("Failed to save config after update check: {}", e);
}
}
}
/// Force an immediate update check (triggered from UI).
///
/// Like the periodic check, the blocking HTTP fetch runs off the main thread
/// via `runtime.spawn_blocking`; the result is applied by
/// `process_update_result` (with `force = true`) when it arrives. Previously
/// this ran `check_now` synchronously on the main thread, freezing all I/O
/// for up to the 30s ureq timeout when the network was slow.
pub fn force_update_check(&mut self) {
let checker = std::sync::Arc::clone(&self.update_checker);
let config = self.config.load_full();
let tx = self.update_check_tx.clone();
self.runtime.spawn_blocking(move || {
let (result, should_save) = checker.check_now(&config, true);
let _ = tx.send(UpdateCheckOutcome {
result,
should_save,
force: true,
});
});
}
/// Force an update check from the settings window. The result is synced back
/// to the settings window asynchronously by `process_update_result` when the
/// off-thread check completes; the settings "Check Now" button no longer
/// blocks the event loop.
pub fn force_update_check_for_settings(&mut self) {
self.force_update_check();
}
/// Detect the installation type and convert to the settings-ui enum.
pub(super) fn detect_installation_type(&self) -> par_term_settings_ui::InstallationType {
let install = crate::self_updater::detect_installation();
match install {
crate::self_updater::InstallationType::Homebrew => {
par_term_settings_ui::InstallationType::Homebrew
}
crate::self_updater::InstallationType::CargoInstall => {
par_term_settings_ui::InstallationType::CargoInstall
}
crate::self_updater::InstallationType::MacOSBundle => {
par_term_settings_ui::InstallationType::MacOSBundle
}
crate::self_updater::InstallationType::StandaloneBinary => {
par_term_settings_ui::InstallationType::StandaloneBinary
}
}
}
/// Send a test desktop notification (for debugging notification support).
pub fn send_test_notification(&self) {
log::info!("Sending test notification");
crate::platform::deliver_desktop_notification(
"par-term Test Notification",
"If you see this, notifications are working!",
5000,
crate::platform::NotificationUrgency::Normal,
);
}
}