Skip to content

Commit 172dcde

Browse files
authored
35 implement update logic to update the application from GitHub (#43)
* Add initial SVG icon for TSS * Replace original logo.svg with new SVG logo for TSS * feat: Implement auto-update system for Trial Submission Studio - Added `tss-updater` crate for managing updates. - Implemented GitHub client for checking and downloading updates. - Added platform detection for macOS, Windows, and Linux. - Created structures for GitHub releases and assets. - Implemented semantic versioning support for version comparisons. - Added checksum verification for downloaded updates. - Included example usage in documentation. - Added macOS packaging files including Info.plist and entitlements.plist. - Added Windows icon and macOS app icon. * Refactor update error handling and remove installer module - Simplified `UpdateError` enum by changing error variants to use strings instead of specific error types. - Removed the `installer.rs` module, consolidating update installation logic into the `UpdateService`. - Updated `lib.rs` to reflect changes in the update service structure and removed unnecessary modules. - Streamlined the update process by utilizing the `self_update` crate for downloading and installing updates. - Removed platform detection logic and related tests, as they are no longer needed with the new update handling approach. - Updated `release.rs` to simplify the `UpdateInfo` structure, focusing on essential fields for UI display.
1 parent e38ef93 commit 172dcde

File tree

24 files changed

+2599
-25
lines changed

24 files changed

+2599
-25
lines changed

.github/workflows/release.yml

Lines changed: 407 additions & 0 deletions
Large diffs are not rendered by default.

Cargo.lock

Lines changed: 399 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ members = [
88
"crates/tss-output",
99
"crates/tss-standards",
1010
"crates/tss-transform",
11+
"crates/tss-updater",
1112
"crates/tss-validate",
1213
"crates/tss-xpt",
1314
]
1415
resolver = "3"
1516

1617
[workspace.package]
17-
version = "0.1.0"
18+
version = "0.0.1"
1819
edition = "2024"
1920
rust-version = "1.92"
21+
license = "MIT"
2022

2123
[workspace.dependencies]
2224
anyhow = "1.0.100"
23-
chrono = "0.4.42"
25+
chrono = { version = "0.4.42", features = ["serde"] }
2426
csv = "1.4.0"
2527
hex = "0.4.3"
2628
insta = { version = "1.45.1", features = ["json"] }

crates/tss-gui/Cargo.toml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ tss-transform = { path = "../tss-transform" }
3030
tss-validate = { path = "../tss-validate" }
3131
tss-output = { path = "../tss-output" }
3232
tss-xpt = { path = "../tss-xpt" }
33+
tss-updater = { path = "../tss-updater" }
3334

3435
# Common dependencies
3536
anyhow.workspace = true
@@ -41,18 +42,20 @@ tracing-subscriber.workspace = true
4142
chrono.workspace = true
4243

4344
# GUI-specific
44-
rfd = "0.16.0" # Native file dialogs
45-
egui-phosphor = "0.11.0" # Icon set
45+
rfd = "0.16.0" # Native file dialogs
46+
egui-phosphor = "0.11.0" # Icon set
47+
egui_commonmark = "0.22.0" # Markdown rendering
48+
open = "5.3" # Open URLs in browser
4649

4750
# Settings & menu
48-
directories = "6.0" # Cross-platform app directories
49-
toml = "0.9.8" # Config file format
50-
muda = "0.17.1" # Native menu bar (by Tauri team)
51+
directories = "6.0" # Cross-platform app directories
52+
toml = "0.9.8" # Config file format
53+
muda = "0.17.1" # Native menu bar (by Tauri team)
5154
crossbeam-channel = "0.5.15" # For muda menu events
5255

5356
# Platform-specific
5457
[target.'cfg(target_os = "macos")'.dependencies]
55-
winit = "0.30.12" # Needed for EventLoopBuilderExtMacOS trait
58+
winit = "0.30.12" # Needed for EventLoopBuilderExtMacOS trait
5659

5760
[lints]
5861
workspace = true

crates/tss-gui/src/app.rs

Lines changed: 199 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,25 @@ use crate::menu;
44
use crate::services::StudyLoader;
55
use crate::settings::ui::{SettingsResult, SettingsWindow};
66
use crate::settings::{load_settings, save_settings};
7-
use crate::state::{AppState, EditorTab, View};
8-
use crate::views::{DomainEditorView, ExportView, HomeAction, HomeView};
7+
use crate::state::{AppState, EditorTab, UpdateDialogState, View};
8+
use crate::views::{
9+
CommonMarkCache, DomainEditorView, ExportView, HomeAction, HomeView, UpdateDialogAction,
10+
show_about_dialog, show_update_dialog,
11+
};
912
use crossbeam_channel::Receiver;
1013
use eframe::egui;
1114
use muda::{Menu, MenuEvent};
15+
use std::sync::mpsc;
16+
use std::thread;
17+
use tss_updater::UpdateService;
18+
19+
/// Message sent from update background thread.
20+
enum UpdateMessage {
21+
/// Update check completed: Ok(Some((version, changelog))) if available, Ok(None) if up-to-date.
22+
CheckResult(Result<Option<(String, String)>, String>),
23+
/// Install failed with error (success means app restarted).
24+
InstallFailed(String),
25+
}
1226

1327
/// Main application struct
1428
pub struct CdiscApp {
@@ -19,6 +33,14 @@ pub struct CdiscApp {
1933
menu: Menu,
2034
/// Settings window UI component
2135
settings_window: SettingsWindow,
36+
/// Markdown cache for rendering changelogs
37+
markdown_cache: CommonMarkCache,
38+
/// Whether we've performed the startup update check
39+
startup_check_done: bool,
40+
/// Channel for receiving update messages from background threads
41+
update_receiver: mpsc::Receiver<UpdateMessage>,
42+
/// Sender for update messages (cloned for background threads)
43+
update_sender: mpsc::Sender<UpdateMessage>,
2244
}
2345

2446
impl CdiscApp {
@@ -37,11 +59,18 @@ impl CdiscApp {
3759
let settings = load_settings();
3860
tracing::info!("Loaded settings: dark_mode={}", settings.general.dark_mode);
3961

62+
// Create update message channel
63+
let (update_sender, update_receiver) = mpsc::channel();
64+
4065
Self {
4166
state: AppState::new(settings),
4267
menu_receiver,
4368
menu,
4469
settings_window: SettingsWindow::default(),
70+
markdown_cache: CommonMarkCache::default(),
71+
startup_check_done: false,
72+
update_receiver,
73+
update_sender,
4574
}
4675
}
4776
}
@@ -51,12 +80,18 @@ impl eframe::App for CdiscApp {
5180
// Handle preview results from background threads
5281
self.handle_preview_results(ctx);
5382

83+
// Handle update messages from background threads
84+
self.handle_update_messages(ctx);
85+
5486
// Handle menu events
5587
self.handle_menu_events(ctx);
5688

5789
// Handle keyboard shortcuts
5890
self.handle_shortcuts(ctx);
5991

92+
// Perform startup update check if needed
93+
self.maybe_startup_update_check(ctx);
94+
6095
// Track home view action
6196
let mut home_action = HomeAction::None;
6297

@@ -82,6 +117,14 @@ impl eframe::App for CdiscApp {
82117
}
83118
}
84119

120+
// Show update dialog if open
121+
let update_action =
122+
show_update_dialog(ctx, &mut self.state.ui.update, &mut self.markdown_cache);
123+
self.handle_update_action(update_action, ctx);
124+
125+
// Show about dialog if open
126+
show_about_dialog(ctx, &mut self.state.ui.about);
127+
85128
// Main panel
86129
egui::CentralPanel::default().show(ctx, |ui| match self.state.view.clone() {
87130
View::Home => {
@@ -129,8 +172,10 @@ impl CdiscApp {
129172
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
130173
}
131174
menu::ids::ABOUT => {
132-
// TODO: Show about dialog
133-
tracing::info!("About clicked");
175+
self.state.ui.about.open();
176+
}
177+
menu::ids::CHECK_UPDATES => {
178+
self.start_update_check(ctx);
134179
}
135180
_ => {
136181
tracing::debug!("Unknown menu event: {}", id);
@@ -287,3 +332,153 @@ impl CdiscApp {
287332
}
288333
}
289334
}
335+
336+
impl CdiscApp {
337+
/// Handle update messages from background threads.
338+
fn handle_update_messages(&mut self, ctx: &egui::Context) {
339+
while let Ok(msg) = self.update_receiver.try_recv() {
340+
match msg {
341+
UpdateMessage::CheckResult(result) => {
342+
match result {
343+
Ok(Some((version, changelog))) => {
344+
tracing::info!("Update available: {}", version);
345+
self.state.ui.update =
346+
UpdateDialogState::UpdateAvailable { version, changelog };
347+
}
348+
Ok(None) => {
349+
tracing::info!("No update available");
350+
// Show "up to date" only if dialog was opened (manual check)
351+
if self.state.ui.update.is_open() {
352+
self.state.ui.update = UpdateDialogState::NoUpdate;
353+
}
354+
}
355+
Err(e) => {
356+
tracing::error!("Update check failed: {}", e);
357+
// Show error only if dialog is open (manual check)
358+
if self.state.ui.update.is_open() {
359+
self.state.ui.update = UpdateDialogState::Error(e);
360+
}
361+
}
362+
}
363+
// Record check time
364+
self.state.settings.updates.record_check();
365+
if let Err(e) = save_settings(&self.state.settings) {
366+
tracing::error!("Failed to save settings: {}", e);
367+
}
368+
}
369+
UpdateMessage::InstallFailed(error) => {
370+
tracing::error!("Update installation failed: {}", error);
371+
self.state.ui.update = UpdateDialogState::Error(error);
372+
}
373+
}
374+
ctx.request_repaint();
375+
}
376+
}
377+
378+
/// Perform startup update check if settings allow it.
379+
fn maybe_startup_update_check(&mut self, ctx: &egui::Context) {
380+
if self.startup_check_done {
381+
return;
382+
}
383+
self.startup_check_done = true;
384+
385+
// Check if we should perform an automatic check
386+
if !self.state.settings.updates.should_check_now() {
387+
tracing::debug!("Skipping startup update check (disabled or recently checked)");
388+
return;
389+
}
390+
391+
tracing::info!("Performing startup update check");
392+
self.start_update_check_background(ctx);
393+
}
394+
395+
/// Start an update check (opens dialog for manual check).
396+
fn start_update_check(&mut self, ctx: &egui::Context) {
397+
// Check rate limiting for manual checks
398+
if !self.state.settings.updates.can_check_manually() {
399+
if let Some(secs) = self
400+
.state
401+
.settings
402+
.updates
403+
.seconds_until_manual_check_allowed()
404+
{
405+
tracing::info!("Manual check rate limited, {} seconds until allowed", secs);
406+
}
407+
return;
408+
}
409+
410+
self.state.ui.update = UpdateDialogState::Checking;
411+
self.start_update_check_background(ctx);
412+
}
413+
414+
/// Start update check in background thread.
415+
fn start_update_check_background(&mut self, ctx: &egui::Context) {
416+
let sender = self.update_sender.clone();
417+
let settings = self.state.settings.updates.clone();
418+
let ctx = ctx.clone();
419+
420+
thread::spawn(move || {
421+
let result = match UpdateService::check_for_update(&settings) {
422+
Ok(Some(info)) => Ok(Some((info.version, info.changelog))),
423+
Ok(None) => Ok(None),
424+
Err(e) => Err(e.to_string()),
425+
};
426+
427+
let _ = sender.send(UpdateMessage::CheckResult(result));
428+
ctx.request_repaint();
429+
});
430+
}
431+
432+
/// Handle actions from the update dialog.
433+
fn handle_update_action(&mut self, action: UpdateDialogAction, ctx: &egui::Context) {
434+
match action {
435+
UpdateDialogAction::None => {}
436+
UpdateDialogAction::SkipVersion => {
437+
// Extract version from current state
438+
if let UpdateDialogState::UpdateAvailable { ref version, .. } = self.state.ui.update
439+
{
440+
self.state.settings.updates.skipped_version = Some(version.clone());
441+
if let Err(e) = save_settings(&self.state.settings) {
442+
tracing::error!("Failed to save settings: {}", e);
443+
}
444+
}
445+
self.state.ui.update.close();
446+
}
447+
UpdateDialogAction::RemindLater => {
448+
self.state.ui.update.close();
449+
}
450+
UpdateDialogAction::InstallAndRestart => {
451+
self.start_install(ctx);
452+
}
453+
UpdateDialogAction::Cancel => {
454+
self.state.ui.update.close();
455+
}
456+
}
457+
}
458+
459+
/// Download, install update, and restart the application.
460+
///
461+
/// Uses self_update for the entire process. On success, the app will restart.
462+
fn start_install(&mut self, ctx: &egui::Context) {
463+
self.state.ui.update = UpdateDialogState::Installing;
464+
465+
let sender = self.update_sender.clone();
466+
let ctx = ctx.clone();
467+
468+
thread::spawn(move || {
469+
// Download and install - self_update handles everything
470+
if let Err(e) = UpdateService::download_and_install() {
471+
let _ = sender.send(UpdateMessage::InstallFailed(e.to_string()));
472+
ctx.request_repaint();
473+
return;
474+
}
475+
476+
// Restart the application
477+
if let Err(e) = UpdateService::restart() {
478+
let _ = sender.send(UpdateMessage::InstallFailed(e.to_string()));
479+
ctx.request_repaint();
480+
}
481+
// On success, the app restarts and this thread ends
482+
});
483+
}
484+
}

crates/tss-gui/src/menu.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use muda::{
1313
pub mod ids {
1414
pub const OPEN_STUDY: &str = "open_study";
1515
pub const SETTINGS: &str = "settings";
16+
pub const CHECK_UPDATES: &str = "check_updates";
1617
pub const ABOUT: &str = "about";
1718
pub const EXIT: &str = "exit";
1819
}
@@ -43,6 +44,20 @@ pub fn create_menu() -> Menu {
4344
.append(&PredefinedMenuItem::separator())
4445
.expect("Failed to add separator");
4546

47+
// Check for Updates
48+
app_menu
49+
.append(&MenuItem::with_id(
50+
ids::CHECK_UPDATES,
51+
"Check for Updates...",
52+
true,
53+
None,
54+
))
55+
.expect("Failed to add Check for Updates menu item");
56+
57+
app_menu
58+
.append(&PredefinedMenuItem::separator())
59+
.expect("Failed to add separator");
60+
4661
// Settings (Cmd+,)
4762
app_menu
4863
.append(&MenuItem::with_id(
@@ -135,6 +150,19 @@ pub fn create_menu() -> Menu {
135150

136151
#[cfg(not(target_os = "macos"))]
137152
{
153+
help_menu
154+
.append(&MenuItem::with_id(
155+
ids::CHECK_UPDATES,
156+
"Check for Updates...",
157+
true,
158+
None,
159+
))
160+
.expect("Failed to add Check for Updates menu item");
161+
162+
help_menu
163+
.append(&PredefinedMenuItem::separator())
164+
.expect("Failed to add separator");
165+
138166
help_menu
139167
.append(&MenuItem::with_id(ids::ABOUT, "About", true, None))
140168
.expect("Failed to add About menu item");

crates/tss-gui/src/settings/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ use serde::{Deserialize, Serialize};
1717
use std::collections::{HashMap, HashSet};
1818
use std::path::PathBuf;
1919

20+
// Re-export update settings from tss-updater
21+
pub use tss_updater::{UpdateChannel, UpdateCheckFrequency, UpdateSettings};
22+
2023
// ============================================================================
2124
// Main Settings Struct
2225
// ============================================================================
@@ -31,6 +34,7 @@ pub struct Settings {
3134
pub export: ExportSettings,
3235
pub display: DisplaySettings,
3336
pub shortcuts: ShortcutSettings,
37+
pub updates: UpdateSettings,
3438

3539
/// Recent study folders (persisted for convenience).
3640
#[serde(default)]
@@ -46,6 +50,7 @@ impl Default for Settings {
4650
export: ExportSettings::default(),
4751
display: DisplaySettings::default(),
4852
shortcuts: ShortcutSettings::default(),
53+
updates: UpdateSettings::default(),
4954
recent_studies: Vec::new(),
5055
}
5156
}

0 commit comments

Comments
 (0)