diff --git a/settings-ui/main.js b/settings-ui/main.js
index 1732e88..b9e383d 100644
--- a/settings-ui/main.js
+++ b/settings-ui/main.js
@@ -53,10 +53,18 @@ async function save() {
function renderAccounts(accounts) {
const list = document.getElementById("accounts-list");
+ const startup = document.getElementById("startup_account");
list.textContent = "";
+ startup.textContent = "";
const canRemove = accounts.length > 1;
for (const a of accounts) {
+ const option = document.createElement("option");
+ option.value = a.id;
+ option.textContent = a.name;
+ option.selected = a.is_default;
+ startup.appendChild(option);
+
const row = document.createElement("div");
row.className = "account-row";
@@ -164,6 +172,15 @@ window.addEventListener("DOMContentLoaded", () => {
wireLock();
document.getElementById("save").addEventListener("click", save);
document.getElementById("add_account").addEventListener("click", addAccount);
+ document.getElementById("startup_account").addEventListener("change", async (event) => {
+ try {
+ await invoke("set_default_account", { id: event.target.value });
+ await loadAccounts();
+ } catch (e) {
+ await Dlg.alert(String(e), { title: "Could not change startup account" });
+ await loadAccounts();
+ }
+ });
document.getElementById("new_account_name").addEventListener("keydown", (e) => {
if (e.key === "Enter") addAccount();
});
diff --git a/src-tauri/src/accounts.rs b/src-tauri/src/accounts.rs
index 0a5527a..30e8c0a 100644
--- a/src-tauri/src/accounts.rs
+++ b/src-tauri/src/accounts.rs
@@ -40,6 +40,8 @@ pub struct Account {
pub struct AccountsFile {
pub accounts: Vec
,
pub next_seq: u32,
+ /// Preferred startup account; independent of the historical `default` profile id.
+ pub startup_account: Option,
}
impl Default for AccountsFile {
@@ -52,6 +54,7 @@ impl Default for AccountsFile {
store_uuid: None,
}],
next_seq: 1,
+ startup_account: None,
}
}
}
@@ -90,7 +93,27 @@ pub fn remove(f: &mut AccountsFile, id: &str) -> Result {
let Some(pos) = f.accounts.iter().position(|a| a.id == id) else {
return Err(format!("unknown account: {id}"));
};
- Ok(f.accounts.remove(pos))
+ let removed = f.accounts.remove(pos);
+ if f.startup_account.as_deref() == Some(id) {
+ f.startup_account = None;
+ }
+ Ok(removed)
+}
+
+/// Older files and a removed preference fall back to the first account in UI order.
+pub fn startup_account(f: &AccountsFile) -> Option<&Account> {
+ f.startup_account
+ .as_deref()
+ .and_then(|id| f.accounts.iter().find(|a| a.id == id))
+ .or_else(|| f.accounts.iter().min_by_key(|a| a.order))
+}
+
+pub fn set_startup_account(f: &mut AccountsFile, id: &str) -> Result<(), String> {
+ if !f.accounts.iter().any(|a| a.id == id) {
+ return Err(format!("unknown account: {id}"));
+ }
+ f.startup_account = Some(id.to_owned());
+ Ok(())
}
/// Rename an account by id. Errors if the id is unknown.
@@ -215,6 +238,53 @@ pub fn gen_store_uuid() -> [u8; 16] {
mod tests {
use super::*;
+ #[test]
+ fn startup_preference_survives_roundtrip_without_changing_profile_identity() {
+ let mut f = AccountsFile::default();
+ let second = add(&mut f, "Work");
+ set_startup_account(&mut f, &second.id).unwrap();
+ let restored: AccountsFile =
+ serde_json::from_str(&serde_json::to_string(&f).unwrap()).unwrap();
+ assert_eq!(startup_account(&restored), Some(&second));
+ assert_eq!(restored.accounts[0].id, "default");
+ assert_eq!(restored.accounts[0].store_uuid, None);
+ }
+
+ #[test]
+ fn missing_or_stale_startup_preference_uses_ui_order() {
+ let mut f = AccountsFile::default();
+ add(&mut f, "Work");
+ f.accounts.reverse();
+ assert_eq!(startup_account(&f).unwrap().id, "default");
+ f.startup_account = Some("removed-id".into());
+ assert_eq!(startup_account(&f).unwrap().id, "default");
+ }
+
+ #[test]
+ fn removing_preferred_account_selects_remaining_account() {
+ let mut f = AccountsFile::default();
+ let second = add(&mut f, "Work");
+ set_startup_account(&mut f, &second.id).unwrap();
+ remove(&mut f, &second.id).unwrap();
+ assert_eq!(startup_account(&f).unwrap().id, "default");
+ assert_eq!(f.startup_account, None);
+ }
+
+ #[test]
+ fn startup_selection_rejects_unknown_id_without_mutating_state() {
+ let mut f = AccountsFile::default();
+ let before = f.clone();
+ assert!(set_startup_account(&mut f, "missing").is_err());
+ assert_eq!(f, before);
+ }
+
+ #[test]
+ fn no_startup_account_when_list_is_empty() {
+ let mut f = AccountsFile::default();
+ f.accounts.clear();
+ assert!(startup_account(&f).is_none());
+ }
+
#[test]
fn default_file_has_single_default_account() {
let f = AccountsFile::default();
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index 63915a9..3feb9e6 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -168,6 +168,7 @@ pub struct AccountView {
pub order: u32,
pub unread: u32,
pub open: bool,
+ pub is_default: bool,
}
fn account_views(app: &tauri::AppHandle) -> Vec {
@@ -185,12 +186,29 @@ fn account_views(app: &tauri::AppHandle) -> Vec {
open: app
.get_webview_window(&accounts::window_label(&a.id))
.is_some(),
+ is_default: accounts::startup_account(&f).is_some_and(|startup| startup.id == a.id),
})
.collect();
views.sort_by_key(|v| v.order);
views
}
+#[tauri::command]
+pub fn set_default_account(
+ window: tauri::Window,
+ app: tauri::AppHandle,
+ id: String,
+) -> Result<(), String> {
+ if is_remote(&window) {
+ return Err("forbidden".into());
+ }
+ lock::require_unlocked(&app)?;
+ let _guard = accounts::lock_mutations();
+ let mut f = accounts::load(&app);
+ accounts::set_startup_account(&mut f, &id)?;
+ accounts::save(&app, &f).map_err(|e| e.to_string())
+}
+
#[tauri::command]
pub fn list_accounts(
window: tauri::Window,
@@ -236,12 +254,14 @@ pub async fn add_account(
}
crate::tray::rebuild_menu(&app);
+ let is_default = accounts::startup_account(&f).is_some_and(|startup| startup.id == acct.id);
Ok(AccountView {
id: acct.id,
name: acct.name,
order: acct.order,
unread: 0,
open: true,
+ is_default,
})
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index acf1a55..f79d690 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -121,6 +121,7 @@ pub fn run() {
commands::set_settings,
commands::open_settings,
commands::list_accounts,
+ commands::set_default_account,
commands::add_account,
commands::remove_account,
commands::rename_account,
@@ -175,9 +176,21 @@ pub fn run() {
handle.manage(lock::LockState::new(!lock_on_launch));
let open_hidden = start_hidden || lock_on_launch;
+ let startup_label =
+ accounts::startup_account(&f).map(|account| accounts::window_label(&account.id));
+ if let Some(label) = &startup_label {
+ *handle.state::().lock().unwrap() = label.clone();
+ }
+
// Open every account window so each one receives messages/notifications.
- for a in &f.accounts {
- window::open_account_window(handle, a, open_hidden)?;
+ // On a normal launch the preferred account is the visible one; the rest
+ // stay connected in the background. A minimized or locked launch keeps
+ // every account hidden.
+ for account in &f.accounts {
+ let is_startup = startup_label
+ .as_deref()
+ .is_some_and(|label| label == accounts::window_label(&account.id));
+ window::open_account_window(handle, account, open_hidden || !is_startup)?;
}
tray::setup(handle)?;
diff --git a/src-tauri/src/lock.rs b/src-tauri/src/lock.rs
index 221665a..9830e13 100644
--- a/src-tauri/src/lock.rs
+++ b/src-tauri/src/lock.rs
@@ -99,6 +99,20 @@ pub fn unlock(app: &AppHandle) {
/// capture of the lock screen; `prevent_close` while locked stops the X from
/// relaunching into an unlocked state.
pub fn show_lock_window(app: &AppHandle) {
+ let app = app.clone();
+ tauri::async_runtime::spawn_blocking(move || {
+ static CREATE: Mutex<()> = Mutex::new(());
+ let _guard = CREATE.lock().unwrap_or_else(|e| e.into_inner());
+ if !is_unlocked(&app) {
+ show_lock_window_inner(&app);
+ }
+ });
+}
+
+fn show_lock_window_inner(app: &AppHandle) {
+ if is_unlocked(app) {
+ return;
+ }
if let Some(w) = app.get_webview_window("lock") {
let _ = w.show();
let _ = w.set_focus();
@@ -116,6 +130,12 @@ pub fn show_lock_window(app: &AppHandle) {
.build();
if let Ok(win) = built {
+ // Unlock can race a queued window creation; never leave a stale lock
+ // screen on top after authentication has already succeeded.
+ if is_unlocked(app) {
+ let _ = win.destroy();
+ return;
+ }
let app_handle = app.clone();
win.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
diff --git a/src-tauri/src/window.rs b/src-tauri/src/window.rs
index f69fe9f..981eca7 100644
--- a/src-tauri/src/window.rs
+++ b/src-tauri/src/window.rs
@@ -1226,16 +1226,41 @@ pub fn toggle_active(app: &AppHandle) {
/// Opens (or focuses) the local settings window.
pub fn open_settings_window(app: &AppHandle) {
+ // Tray/menu callbacks can deadlock WebView2 when they construct a window
+ // synchronously. Serialize construction on a blocking worker instead.
+ let app = app.clone();
+ tauri::async_runtime::spawn_blocking(move || {
+ static CREATE: std::sync::Mutex<()> = std::sync::Mutex::new(());
+ let _guard = CREATE.lock().unwrap_or_else(|e| e.into_inner());
+ open_settings_window_inner(&app);
+ });
+}
+
+fn open_settings_window_inner(app: &AppHandle) {
+ if !crate::lock::is_unlocked(app) {
+ crate::lock::show_lock_window(app);
+ return;
+ }
if let Some(w) = app.get_webview_window("settings") {
let _ = w.show();
let _ = w.set_focus();
return;
}
- let _ = WebviewWindowBuilder::new(app, "settings", WebviewUrl::App("index.html".into()))
+ let built = WebviewWindowBuilder::new(app, "settings", WebviewUrl::App("index.html".into()))
.title("whatRust — Settings")
.inner_size(440.0, 680.0)
.resizable(false)
.build();
+ if let Ok(window) = built {
+ // A lock request can win while the settings webview is queued.
+ if crate::lock::is_unlocked(app) {
+ let _ = window.show();
+ let _ = window.set_focus();
+ } else {
+ let _ = window.destroy();
+ crate::lock::show_lock_window(app);
+ }
+ }
}
#[cfg(test)]