diff --git a/crates/pdfs-core/src/auth.rs b/crates/pdfs-core/src/auth.rs index a7a558d..3f71e24 100644 --- a/crates/pdfs-core/src/auth.rs +++ b/crates/pdfs-core/src/auth.rs @@ -12,7 +12,7 @@ use proton_sdk::config::ProtonClientConfiguration; use proton_sdk::session::{PasswordMode, ProtonApiSession, ResumeParameters}; use serde::{Deserialize, Serialize}; -use crate::config::{APP_VERSION, KEYRING_SERVICE, USER_AGENT}; +use crate::config::{AppDirs, APP_VERSION, KEYRING_SERVICE, USER_AGENT}; use crate::error::{Error, Result}; /// Fixed keyring account name for the single stored session blob. @@ -53,7 +53,14 @@ impl StoredSession { } fn client_config() -> ProtonClientConfiguration { - ProtonClientConfiguration::new(APP_VERSION).with_user_agent(USER_AGENT) + let (app_version, user_agent) = match AppDirs::new() { + Ok(dirs) => { + let config = dirs.load_config(); + (config.app_version, config.user_agent) + } + Err(_) => (APP_VERSION.to_string(), USER_AGENT.to_string()), + }; + ProtonClientConfiguration::new(app_version).with_user_agent(user_agent) } fn keyring_entry() -> Result { diff --git a/crates/pdfs-core/src/config.rs b/crates/pdfs-core/src/config.rs index 4dc7a2a..c908f7b 100644 --- a/crates/pdfs-core/src/config.rs +++ b/crates/pdfs-core/src/config.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; @@ -18,6 +19,22 @@ pub const KEYRING_SERVICE: &str = "proton-drive-linux"; /// blobs back under this; pinned files are exempt. See [`crate::cache`]. pub const DEFAULT_CACHE_BUDGET_BYTES: u64 = 5 * 1024 * 1024 * 1024; +/// Configuration structure allowing the user to customize client identification headers. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AppConfig { + pub app_version: String, + pub user_agent: String, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + app_version: APP_VERSION.to_string(), + user_agent: USER_AGENT.to_string(), + } + } +} + /// Resolved XDG locations for state, cache, and the default mountpoint. pub struct AppDirs { dirs: ProjectDirs, @@ -30,6 +47,30 @@ impl AppDirs { Ok(Self { dirs }) } + /// Configuration file path (e.g. ~/.config/proton-drive-linux/config.json). + pub fn config_path(&self) -> PathBuf { + self.dirs.config_dir().join("config.json") + } + + /// Load config from disk, creating default if missing. + pub fn load_config(&self) -> AppConfig { + let path = self.config_path(); + if path.exists() { + if let Ok(content) = std::fs::read_to_string(&path) { + if let Ok(config) = serde_json::from_str::(&content) { + return config; + } + } + } + + let default_config = AppConfig::default(); + if let Ok(content) = serde_json::to_string_pretty(&default_config) { + let _ = std::fs::create_dir_all(self.dirs.config_dir()); + let _ = std::fs::write(&path, content); + } + default_config + } + /// Persistent state: inode map / cache index DB lives here. pub fn state_dir(&self) -> PathBuf { // `state_dir` is Linux-only in `directories`; fall back to data dir. @@ -59,6 +100,11 @@ impl AppDirs { self.state_dir().join("control.sock") } + /// Unix domain socket the tray uses to ensure single instance. + pub fn tray_socket(&self) -> PathBuf { + self.state_dir().join("tray.sock") + } + /// Default mountpoint when the user does not pass one explicitly. pub fn default_mountpoint(&self) -> PathBuf { directories::UserDirs::new() @@ -70,6 +116,31 @@ impl AppDirs { pub fn ensure(&self) -> Result<()> { std::fs::create_dir_all(self.state_dir())?; std::fs::create_dir_all(self.cache_dir())?; + let _ = std::fs::create_dir_all(self.dirs.config_dir()); Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_app_config_serialization() { + let config = AppConfig { + app_version: "external-drive-test-client@1.0.0".to_string(), + user_agent: "test-agent/1.0".to_string(), + }; + let json = serde_json::to_string(&config).unwrap(); + let decoded: AppConfig = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.app_version, "external-drive-test-client@1.0.0"); + assert_eq!(decoded.user_agent, "test-agent/1.0"); + } + + #[test] + fn test_default_app_config() { + let default_config = AppConfig::default(); + assert_eq!(default_config.app_version, APP_VERSION); + assert_eq!(default_config.user_agent, USER_AGENT); + } +} diff --git a/crates/pdfs-gui/src/app.rs b/crates/pdfs-gui/src/app.rs index 8e4ecdc..b7cd403 100644 --- a/crates/pdfs-gui/src/app.rs +++ b/crates/pdfs-gui/src/app.rs @@ -93,11 +93,22 @@ fn main() -> glib::ExitCode { .init(); let app = adw::Application::builder().application_id(APP_ID).build(); - app.connect_startup(|_| load_proton_theme()); + app.connect_startup(|_| { + load_proton_theme(); + spawn_tray(); + }); app.connect_activate(build_window); app.run() } +/// Spawn the tray icon process in the background. +fn spawn_tray() { + match Command::new("pdfs-tray").spawn() { + Ok(_) => tracing::info!("spawned `pdfs-tray`"), + Err(e) => tracing::error!("failed to spawn `pdfs-tray`: {e}"), + } +} + /// Install a CSS provider that overrides libadwaita's accent colour with Proton /// purple, app-wide. Named-colour overrides recolour the stock widgets (switch, /// buttons, progress fill) without per-widget styling. diff --git a/crates/pdfs-gui/src/main.rs b/crates/pdfs-gui/src/main.rs index 4ffd7fa..fdd39b6 100644 --- a/crates/pdfs-gui/src/main.rs +++ b/crates/pdfs-gui/src/main.rs @@ -180,6 +180,17 @@ fn main() -> Result<()> { .init(); let dirs = AppDirs::new().context("resolve app dirs")?; + + // Ensure only one instance of the tray runs. + let tray_sock = dirs.tray_socket(); + if std::os::unix::net::UnixStream::connect(&tray_sock).is_ok() { + tracing::info!("Another instance of pdfs-tray is already running; exiting."); + return Ok(()); + } + let _ = std::fs::remove_file(&tray_sock); + let _lock_socket = std::os::unix::net::UnixListener::bind(&tray_sock) + .context("failed to bind tray single-instance socket")?; + let socket = dirs.control_socket(); let default_mountpoint = dirs.default_mountpoint(); diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index 62e0766..63c4538 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -20,7 +20,7 @@ provides=('pdfs') # Files-on-demand mount + keyring need these at runtime. optdepends=('gnome-keyring: credential storage via Secret Service' 'xdg-utils: "Open folder" action from the tray') -options=('!lto') +options=('!lto' '!debug') # Built in place from the checkout; nothing to fetch or checksum. source=() @@ -56,6 +56,8 @@ package() { install -Dm644 "packaging/io.narl.proton-drive-linux.desktop" \ "$pkgdir/usr/share/applications/io.narl.proton-drive-linux.desktop" + install -Dm644 "packaging/io.narl.proton-drive-linux-tray.desktop" \ + "$pkgdir/etc/xdg/autostart/io.narl.proton-drive-linux-tray.desktop" install -Dm644 "packaging/io.narl.proton-drive-linux.svg" \ "$pkgdir/usr/share/icons/hicolor/scalable/apps/io.narl.proton-drive-linux.svg" diff --git a/packaging/io.narl.proton-drive-linux-tray.desktop b/packaging/io.narl.proton-drive-linux-tray.desktop new file mode 100644 index 0000000..92d53ea --- /dev/null +++ b/packaging/io.narl.proton-drive-linux-tray.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Name=Proton Drive (Tray) +GenericName=Cloud Drive Tray +Comment=Proton Drive system tray icon +Exec=pdfs-tray +Icon=io.narl.proton-drive-linux +Terminal=false +Categories=Network;FileTransfer;GTK; +Keywords=proton;drive;cloud;sync;storage; +NoDisplay=true diff --git a/packaging/io.narl.proton-drive-linux.svg b/packaging/io.narl.proton-drive-linux.svg index 5f9afd8..8bd280f 100644 --- a/packaging/io.narl.proton-drive-linux.svg +++ b/packaging/io.narl.proton-drive-linux.svg @@ -10,5 +10,5 @@ - +