authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-01 22:36:38-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-01 22:42:02-07:00
log426cd8cd8b10a8ebc02af363750ce3144677e062
tree40535f8eaef7864964c35130108ab48e796d5f4b
parent83112c0bfd59fdfe0b0ea81e1fbc64ea9839a008
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: keep sync advice while working offline

Turning Work Offline on during a problem state dropped the advice line, shifting everything below it. The advice now stays, and the toggle test covers every state, password-protected and unreadable sections included. Assisted-by: claude-opus-5.5

1 files changed, 35 insertions(+), 31 deletions(-)

crates/snowbound/src/sync.rs+35-31
......@@ -441,28 +441,25 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
441441 let host = facts.place.first().map_or("the notebook", String::as_str);
442442 let waiting = changes(sync.queued);
443443 let conflict;
444 let advice = if facts.offline {
445 None
446 } else {
447 match state {
448 SyncState::NotConnected => Some(match sync.queued {
449 0 => format!("Can’t reach {host}. Sync continues when it’s back."),
450 _ => format!("Can’t reach {host}. {waiting} will sync when it’s back."),
451 }),
452 SyncState::InUse => {
453 Some("Someone else is saving a section. Sync continues when they finish.".into())
454 }
455 SyncState::ReadOnly => Some(format!(
456 "This location is read-only. Your changes stay on {THIS}."
457 )),
458 SyncState::Protected => {
459 Some("Snowbound can’t open password-protected sections yet.".into())
460 }
461 SyncState::Unreadable | SyncState::Failed => {
462 Some("Snowbound keeps trying. Sync Now tries again at once.".into())
463 }
464 _ => None,
444 // A problem's advice stays while working offline, so turning it on moves nothing.
445 let advice = match state {
446 SyncState::NotConnected => Some(match sync.queued {
447 0 => format!("Can’t reach {host}. Sync continues when it’s back."),
448 _ => format!("Can’t reach {host}. {waiting} will sync when it’s back."),
449 }),
450 SyncState::InUse => {
451 Some("Someone else is saving a section. Sync continues when they finish.".into())
465452 }
453 SyncState::ReadOnly => Some(format!(
454 "This location is read-only. Your changes stay on {THIS}."
455 )),
456 SyncState::Protected => {
457 Some("Snowbound can’t open password-protected sections yet.".into())
458 }
459 SyncState::Unreadable | SyncState::Failed => {
460 Some("Snowbound keeps trying. Sync Now tries again at once.".into())
461 }
462 _ => None,
466463 };
467464 if !showing && headline == "Up to date" && !facts.conflicts.is_empty() {
468465 conflict = match facts.conflicts.as_slice() {
......@@ -1380,6 +1377,18 @@ mod tests {
13801377 _ => status(true, 0, None),
13811378 }))
13821379 }),
1380 ("protected", || {
1381 facts(sections(|index| match index {
1382 1 => status(true, 0, Some((io::ErrorKind::Unsupported, "Protected"))),
1383 _ => status(true, 0, None),
1384 }))
1385 }),
1386 ("unreadable", || {
1387 facts(sections(|index| match index {
1388 4 => status(true, 0, Some((io::ErrorKind::InvalidData, "Not a section"))),
1389 _ => status(true, 0, None),
1390 }))
1391 }),
13831392 ("in-use", || {
13841393 facts(sections(|index| match index {
13851394 2 => status(true, 2, Some((io::ErrorKind::ResourceBusy, "In use"))),
......@@ -1433,17 +1442,12 @@ mod tests {
14331442 )))
14341443 }
14351444
1436 /// Work offline's switch stays put as it turns on and off, and so do the buttons below
1437 /// but where a problem's advice, which offline leaves out, goes and comes back.
1445 /// Work offline's switch, and everything below it, stays put as it turns on and off.
14381446 #[test]
14391447 fn toggling_offline_moves_nothing() {
14401448 let switch = id().child("status").child("header").child("offline");
14411449 let controls = id().child("controls");
14421450 for (name, made) in states() {
1443 let advised = !matches!(
1444 overall(&made().sections).state(),
1445 SyncState::UpToDate | SyncState::Syncing
1446 );
14471451 let mut places = Vec::new();
14481452 for offline in [false, true, false] {
14491453 let mut ui = ui(Appearance::Light);
......@@ -1452,10 +1456,10 @@ mod tests {
14521456 settle(&mut ui, &mut now, &facts);
14531457 places.push((ui.rect(switch).unwrap(), ui.rect(controls).unwrap()));
14541458 }
1455 for pair in places.windows(2) {
1456 assert_eq!(pair[0].0, pair[1].0, "{name}");
1457 assert!(advised || pair[0].1 == pair[1].1, "{name}: {places:?}");
1458 }
1459 assert!(
1460 places.windows(2).all(|pair| pair[0] == pair[1]),
1461 "{name}: {places:?}"
1462 );
14591463 }
14601464 }
14611465