From 282b96db3198a95fdb61aa5d1977a7b2c9e05828 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 2 Oct 2026 11:14:59 -0700 Subject: [PATCH] fix: A launch returns to the page each notebook was left on The settings kept the pages shown lately, but a launch opened the shown notebook at its first section and page, and so did every section and notebook opened after. The launch now shows the latest of those pages in the shown notebook, and sections and notebooks opened later return to theirs. Assisted-by: claude-opus-5.5 --- crates/snowbound/src/main.rs | 21 +++++++++--- crates/snowbound/src/settings.rs | 2 +- crates/snowbound/src/sidebar.rs | 20 +++++++++--- crates/snowbound/tests/replay.rs | 55 ++++++++++++++++++++++++++++---- 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/crates/snowbound/src/main.rs b/crates/snowbound/src/main.rs index 5aab659e493d1bb8a6f24c17f64e19de201386d9..769853cc4bf9b254a1441c15056cf406c5320a41 100644 --- a/crates/snowbound/src/main.rs +++ b/crates/snowbound/src/main.rs @@ -1039,23 +1039,32 @@ impl State { library.purge_recycle_bin(); } } + // Where the notebook was left, or its first section. let shown = notebooks .iter() .filter(|library| Some(&library.location) == current.as_ref()) .chain(¬ebooks) - .find_map(|library| Some((library, library.first_section()?))); + .find_map(|library| { + let left = stored.recent.iter().find(|place| { + place.notebook == library.location && library.contains(&place.section) + }); + match left { + Some(place) => Some((library, place.section.clone(), Some(place.page))), + None => Some((library, library.first_section()?, None)), + } + }); match shown { - Some((library, path)) if !library.locked(&path) => { + Some((library, path, left)) if !library.locked(&path) => { let section = library.open(&path, notify(proxy.clone()))?; let (opened, page) = - read_session(section, Arc::clone(library), path, None)?; + read_session(section, Arc::clone(library), path, left)?; let (scene, editor) = PageScene::from_page(page, &mut engine)?; session = Some(opened); (editor, Some((scene, [0.0; 2]))) } shown => { // A first section still locked shows its locked page. - locked = shown.map(|(library, path)| protection::Locked { + locked = shown.map(|(library, path, _)| protection::Locked { library: Arc::clone(library), path, }); @@ -1228,7 +1237,9 @@ impl State { opening: None, switching: None, places: HashMap::new(), - last_pages: HashMap::new(), + last_pages: (stored.recent.iter().rev()) + .map(|place| (library::key(&place.notebook, &place.section), place.page)) + .collect(), trail: { let mut trail = navigation::Trail::default(); trail.recent = stored.recent; diff --git a/crates/snowbound/src/settings.rs b/crates/snowbound/src/settings.rs index 569cd42bed2fe5b6812132af90cd4e9785c81d0f..55518123a37062e05177d016933d8c6587090826 100644 --- a/crates/snowbound/src/settings.rs +++ b/crates/snowbound/src/settings.rs @@ -46,7 +46,7 @@ pub struct Settings { pub servers: Vec, /// Chords set in Options' Keyboard, by command, each list in place of its defaults. pub keys: std::collections::BTreeMap>, - /// Pages shown lately, latest first, which the palette lists first. + /// Pages shown lately, latest first, which the palette lists first and a launch returns to. pub recent: Vec, pub passwords: Passwords, } diff --git a/crates/snowbound/src/sidebar.rs b/crates/snowbound/src/sidebar.rs index 743fddbe1588504d3d60c74e061bbbea01c217a1..140ce8754ba58f5897bf2dc0c1f32a92679b739d 100644 --- a/crates/snowbound/src/sidebar.rs +++ b/crates/snowbound/src/sidebar.rs @@ -1100,8 +1100,8 @@ impl crate::State { self.read_notebook(location, section, open, read); } - /// Shows `section`, or the first section, of the notebook at `location`, `open` or read - /// with `read`, in place of the notebook listed there. + /// Shows `section`, or where it was left, or the first section, of the notebook at + /// `location`, `open` or read with `read`, in place of the notebook listed there. pub(crate) fn read_notebook( &mut self, location: String, @@ -1109,6 +1109,15 @@ impl crate::State { open: Option>, read: impl FnOnce(&str, &std::path::Path) -> Result + Send + 'static, ) { + let section = section.or_else(|| { + (self.trail.recent.iter()) + .find(|place| place.notebook == location) + .map(|place| place.section.clone()) + }); + let left = section + .as_ref() + .and_then(|path| self.last_pages.get(&crate::library::key(&location, path))) + .copied(); let (cache, notify) = (self.cache.clone(), crate::notify(self.proxy.clone())); self.load(move || { let library = match open { @@ -1122,9 +1131,10 @@ impl crate::State { if let Err(error) = &library.notebook { return Err(error.clone().into()); } - let Some(path) = section + let Some((path, left)) = section .filter(|path| library.contains(path)) - .or_else(|| library.first_section()) + .map(|path| (path, left)) + .or_else(|| Some((library.first_section()?, None))) else { // Shown once its sections arrive. if library.downloading() { @@ -1133,7 +1143,7 @@ impl crate::State { return Err("This folder holds no notebook sections.".into()); }; let section = library.open(&path, notify)?; - let (session, page) = crate::read_session(section, library, path, None)?; + let (session, page) = crate::read_session(section, library, path, left)?; Ok(crate::Loaded::Section(Box::new(session), page)) }); } diff --git a/crates/snowbound/tests/replay.rs b/crates/snowbound/tests/replay.rs index 5e19e767a0881677c33a52b734045914410cec8e..b867e7bb24f1d887ff62a3b0771c3ac9c15601d3 100644 --- a/crates/snowbound/tests/replay.rs +++ b/crates/snowbound/tests/replay.rs @@ -12,6 +12,11 @@ impl Scratch { let path = std::env::temp_dir().join(format!("snowbound-{name}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&path); std::fs::create_dir_all(path.join("notebook")).unwrap(); + std::fs::write( + path.join("settings.json"), + r#"{"user_name": "Snowbound Test"}"#, + ) + .unwrap(); Self(path) } } @@ -22,7 +27,8 @@ impl Drop for Scratch { } } -/// Runs `steps` against a copy of `notebook`'s files, then returns the trees each +/// Runs `steps` against a copy of `notebook`'s files with the scratch's settings, then +/// returns the trees each /// `accessibility` step named by the trailing word wrote. fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec { let dir = &scratch.0; @@ -46,11 +52,6 @@ fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec