authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 02:20:50-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 03:11:20-07:00
log12d02ba99c1881032feaeb079d60181d0f066945
tree4b1ea68a101f3ad01c63249e5639cd94e42f9879
parentc499253d301cfd18a2f657b7ede11bacbe7f855f
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: the sidebar offers Options everywhere and no New iCloud Notebook

The desktop sidebar's footer now reads New Notebook, Open Existing and Options, as the web build's does; macOS left Options to the application menu. New iCloud Notebook goes from the footer and the welcome page with its naming dialog: New Notebook's panel already opens in Snowbound's iCloud Drive folder, and a notebook saved there appears on every Mac by itself. iOS already offers one New Notebook that creates in iCloud Drive where it is on. Assisted-by: claude-opus-5.5

4 files changed, 36 insertions(+), 205 deletions(-)

crates/snowbound/src/main.rs-7
...@@ -653,8 +653,6 @@ enum Command {...@@ -653,8 +653,6 @@ enum Command {
653 OpenFromServer(Option<String>),653 OpenFromServer(Option<String>),
654 /// Asks where to keep a new notebook and creates it.654 /// Asks where to keep a new notebook and creates it.
655 NewNotebook,655 NewNotebook,
656 /// Asks for a name and creates a notebook at the top of the app's iCloud Drive folder.
657 NewICloudNotebook,
658 /// Opens the Snowbound Guide from the user's documents, copying it there first.656 /// Opens the Snowbound Guide from the user's documents, copying it there first.
659 OpenGuide,657 OpenGuide,
660 /// Adds Snowbound to the app menu.658 /// Adds Snowbound to the app menu.
...@@ -801,8 +799,6 @@ struct State {...@@ -801,8 +799,6 @@ struct State {
801 peers: Option<live::Peers>,799 peers: Option<live::Peers>,
802 /// Open Notebook from Server while it is open.800 /// Open Notebook from Server while it is open.
803 server: Option<server::Connect>,801 server: Option<server::Connect>,
804 /// New iCloud Notebook while it is open.
805 new_icloud: Option<manage::Naming>,
806 /// Notebooks in iCloud Drive a thread is reading, or following the download of, by802 /// Notebooks in iCloud Drive a thread is reading, or following the download of, by
807 /// location.803 /// location.
808 icloud_reading: HashSet<String>,804 icloud_reading: HashSet<String>,
...@@ -1178,7 +1174,6 @@ impl State {...@@ -1178,7 +1174,6 @@ impl State {
1178 #[cfg(feature = "live")]1174 #[cfg(feature = "live")]
1179 peers: None,1175 peers: None,
1180 server: None,1176 server: None,
1181 new_icloud: None,
1182 icloud_reading: HashSet::new(),1177 icloud_reading: HashSet::new(),
1183 tags: stored1178 tags: stored
1184 .tags1179 .tags
...@@ -1347,7 +1342,6 @@ impl State {...@@ -1347,7 +1342,6 @@ impl State {
1347 self.themes_dialog();1342 self.themes_dialog();
1348 self.print_dialog();1343 self.print_dialog();
1349 self.server_dialog();1344 self.server_dialog();
1350 self.new_icloud_dialog();
1351 self.link_dialog()?;1345 self.link_dialog()?;
1352 self.properties_dialog();1346 self.properties_dialog();
1353 self.symbols_dialog();1347 self.symbols_dialog();
...@@ -3221,7 +3215,6 @@ impl State {...@@ -3221,7 +3215,6 @@ impl State {
3221 ),3215 ),
3222 Command::OpenFromServer(location) => self.open_server(location.as_deref()),3216 Command::OpenFromServer(location) => self.open_server(location.as_deref()),
3223 Command::NewNotebook => self.new_notebook(),3217 Command::NewNotebook => self.new_notebook(),
3224 Command::NewICloudNotebook => self.new_icloud_notebook(),
3225 Command::OpenGuide => self.open_guide()?,3218 Command::OpenGuide => self.open_guide()?,
3226 #[cfg(target_os = "linux")]3219 #[cfg(target_os = "linux")]
3227 Command::Install => desktop::install(),3220 Command::Install => desktop::install(),
crates/snowbound/src/manage.rs+1-137
...@@ -65,20 +65,6 @@ pub enum Structure {...@@ -65,20 +65,6 @@ pub enum Structure {
65 },65 },
66}66}
6767
68/// New iCloud Notebook while it is open: the name typed, and why the last was refused.
69pub struct Naming {
70 name: String,
71 problem: Option<String>,
72}
73
74fn naming() -> ui::Id {
75 ui::Id::ROOT.child("new icloud notebook")
76}
77
78fn naming_field() -> ui::Id {
79 naming().child("name")
80}
81
82/// Whether the notebook at `location` is a folder at the top of the app's iCloud Drive folder,68/// Whether the notebook at `location` is a folder at the top of the app's iCloud Drive folder,
83/// which the sidebar lists as long as it is there.69/// which the sidebar lists as long as it is there.
84pub fn in_icloud_folder(location: &str) -> bool {70pub fn in_icloud_folder(location: &str) -> bool {
...@@ -260,7 +246,7 @@ impl State {...@@ -260,7 +246,7 @@ impl State {
260 }246 }
261247
262 /// Asks where to create a notebook and creates it, as File, New does in OneNote; the248 /// Asks where to create a notebook and creates it, as File, New does in OneNote; the
263 /// panel starts in iCloud Drive, as Notes keeps notes there.249 /// panel starts in Snowbound's iCloud Drive folder, whose notebooks every Mac lists.
264 pub(crate) fn new_notebook(&mut self) {250 pub(crate) fn new_notebook(&mut self) {
265 let icloud = crate::icloud::folder().or_else(crate::icloud::drive);251 let icloud = crate::icloud::folder().or_else(crate::icloud::drive);
266 let reply = self.reply(|state, root| state.create_notebook(root));252 let reply = self.reply(|state, root| state.create_notebook(root));
...@@ -273,128 +259,6 @@ impl State {...@@ -273,128 +259,6 @@ impl State {
273 );259 );
274 }260 }
275261
276 /// New iCloud Notebook: asks for a name, the first free of "iCloud Notebook", "iCloud
277 /// Notebook 2"…, for a notebook at the top of the app's iCloud Drive folder.
278 pub(crate) fn new_icloud_notebook(&mut self) {
279 let taken: Vec<String> = self
280 .notebooks
281 .iter()
282 .map(|library| library.name.clone())
283 .collect();
284 self.new_icloud = Some(Naming {
285 name: unused(&taken, "iCloud Notebook", false),
286 problem: None,
287 });
288 self.ui.open_popup(naming());
289 self.ui.focus_all(naming_field());
290 }
291
292 /// Builds New iCloud Notebook while it is open.
293 pub(crate) fn new_icloud_dialog(&mut self) {
294 use ui::{Anchor, Axis, Spec, children, fill, px};
295 let Some(dialog) = &mut self.new_icloud else {
296 return;
297 };
298 if !self.ui.popup_open(naming()) {
299 self.new_icloud = None;
300 return;
301 }
302 let ui = &mut self.ui;
303 let theme = ui.theme.clone();
304 let row = theme.font_size * 2.0;
305 let entered =
306 ui::popup::navigation(ui, &[naming_field()], &[winit::keyboard::NamedKey::Enter])
307 .contains(&winit::keyboard::NamedKey::Enter);
308 ui.open_as(
309 naming(),
310 Spec {
311 axis: Axis::Y,
312 size: [px(320.0), children()],
313 fill: Some(theme.popup),
314 border: Some(theme.chip),
315 shadow: Some(theme.shadow),
316 radius: 8.0,
317 pad: [16.0, 12.0],
318 gap: 6.0,
319 anchor: Some(Anchor::Dialog),
320 role: Some(accesskit::Role::Dialog),
321 ..Spec::default()
322 },
323 );
324 if let Some(node) = ui.access(naming()) {
325 node.set_label("New iCloud Notebook");
326 }
327 ui.leaf(
328 "title",
329 Spec {
330 size: [fill(), px(row)],
331 text: Some("New iCloud Notebook"),
332 bold: true,
333 role: Some(accesskit::Role::Heading),
334 ..Spec::default()
335 },
336 );
337 ui::text_field(
338 ui,
339 naming_field(),
340 &mut dialog.name,
341 "",
342 Spec {
343 size: [fill(), px(row)],
344 fill: Some(theme.base),
345 border: Some(theme.accent),
346 radius: 4.0,
347 pad: [6.0, 0.0],
348 ..Spec::default()
349 },
350 );
351 crate::name(ui, naming_field(), "Name");
352 if let Some(problem) = &dialog.problem {
353 ui.leaf(
354 "problem",
355 Spec {
356 size: [fill(), ui::fit()],
357 text: Some(problem),
358 overflow: ui::Overflow::Wrap,
359 pad: [0.0, 4.0],
360 role: Some(accesskit::Role::Status),
361 ..Spec::default()
362 },
363 );
364 }
365 crate::buttons(ui);
366 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
367 let create = ui::button(ui, "create", "Create").clicked || entered;
368 ui.close();
369 ui.close();
370 if cancel {
371 self.ui.close_popup(naming());
372 self.ui.set_focus(Some(crate::page()));
373 self.new_icloud = None;
374 } else if create {
375 let name = dialog.name.trim().to_owned();
376 let folder = crate::icloud::folder();
377 dialog.problem = match &folder {
378 None => Some("Turn on iCloud Drive in System Settings.".into()),
379 Some(_) if name.is_empty() => Some("Enter a name.".into()),
380 Some(_) if name.starts_with('.') || name.contains(['/', ':']) => {
381 Some("Use a name without “/”, “:” or a leading “.”.".into())
382 }
383 Some(folder) if folder.join(&name).exists() => Some(format!(
384 "“{name}” is already in iCloud Drive. Choose another name."
385 )),
386 Some(_) => None,
387 };
388 if let (None, Some(folder)) = (&dialog.problem, folder) {
389 self.ui.close_popup(naming());
390 self.new_icloud = None;
391 if let Err(error) = self.create_notebook(folder.join(name)) {
392 platform::alert("Couldn't create the notebook", &error.to_string());
393 }
394 }
395 }
396 }
397
398 /// Lists the folders at the top of the app's iCloud Drive folder in the sidebar, each a262 /// Lists the folders at the top of the app's iCloud Drive folder in the sidebar, each a
399 /// notebook, as they come and go, and brings down what iCloud Drive keeps elsewhere of263 /// notebook, as they come and go, and brings down what iCloud Drive keeps elsewhere of
400 /// every notebook in it.264 /// every notebook in it.
crates/snowbound/src/sidebar.rs+34-60
...@@ -31,11 +31,9 @@ pub enum Action {...@@ -31,11 +31,9 @@ pub enum Action {
31 /// Folds or unfolds a notebook's or section group's rows, by `Library::key`.31 /// Folds or unfolds a notebook's or section group's rows, by `Library::key`.
32 Fold(String),32 Fold(String),
33 NewNotebook,33 NewNotebook,
34 NewICloudNotebook,
35 OpenNotebook,34 OpenNotebook,
36 /// Signs in again to the notebook opened from its server at this location.35 /// Signs in again to the notebook opened from its server at this location.
37 SignIn(String),36 SignIn(String),
38 /// Opens the Options dialog, which macOS reaches from the application menu instead.
39 Options,37 Options,
40 /// A row's context menu, opened here.38 /// A row's context menu, opened here.
41 Menu(Target, [f32; 2]),39 Menu(Target, [f32; 2]),
...@@ -251,23 +249,11 @@ fn sidebar(...@@ -251,23 +249,11 @@ fn sidebar(
251 ..Spec::default()249 ..Spec::default()
252 },250 },
253 );251 );
254 let options = ("options", art::OPTIONS, "Options", Action::Options);252 for (part, icon, label, chosen) in [
255 let icloud = crate::icloud::folder().map(|_| {253 ("new", art::PLUS, "New Notebook", Action::NewNotebook),
256 (254 ("open", art::NOTEBOOK, "Open Existing", Action::OpenNotebook),
257 "new icloud",255 ("options", art::OPTIONS, "Options", Action::Options),
258 art::ICLOUD,256 ] {
259 "New iCloud Notebook",
260 Action::NewICloudNotebook,
261 )
262 });
263 for (part, icon, label, chosen) in icloud
264 .into_iter()
265 .chain([
266 ("new", art::PLUS, "New Notebook", Action::NewNotebook),
267 ("open", art::NOTEBOOK, "Open Existing", Action::OpenNotebook),
268 ])
269 .chain((!cfg!(target_os = "macos")).then_some(options))
270 {
271 let (row, _) = tree_row(257 let (row, _) = tree_row(
272 ui,258 ui,
273 tree,259 tree,
...@@ -815,9 +801,6 @@ impl crate::State {...@@ -815,9 +801,6 @@ impl crate::State {
815 }801 }
816 Some(Action::Renamed(keep)) => self.finish_renaming(keep),802 Some(Action::Renamed(keep)) => self.finish_renaming(keep),
817 Some(Action::NewNotebook) => self.commands.push(crate::Command::NewNotebook),803 Some(Action::NewNotebook) => self.commands.push(crate::Command::NewNotebook),
818 Some(Action::NewICloudNotebook) => {
819 self.commands.push(crate::Command::NewICloudNotebook)
820 }
821 Some(Action::OpenNotebook) => self.commands.push(crate::Command::OpenNotebook),804 Some(Action::OpenNotebook) => self.commands.push(crate::Command::OpenNotebook),
822 Some(Action::SignIn(location)) => self805 Some(Action::SignIn(location)) => self
823 .commands806 .commands
...@@ -1174,44 +1157,35 @@ impl crate::State {...@@ -1174,44 +1157,35 @@ impl crate::State {
1174 theme.base,1157 theme.base,
1175 "No notebooks open",1158 "No notebooks open",
1176 &servers,1159 &servers,
1177 crate::icloud::folder()1160 [
1178 .map(|_| {1161 (
1179 (1162 "new",
1180 "icloud",1163 art::PLUS,
1181 art::ICLOUD,1164 "New Notebook",
1182 "New iCloud Notebook",1165 crate::Command::NewNotebook,
1183 crate::Command::NewICloudNotebook,1166 ),
1184 )1167 (
1185 })1168 "open",
1186 .into_iter()1169 art::NOTEBOOK,
1187 .chain([1170 "Open Existing",
1188 (1171 crate::Command::OpenNotebook,
1189 "new",1172 ),
1190 art::PLUS,1173 (
1191 "New Notebook",1174 "server",
1192 crate::Command::NewNotebook,1175 art::SERVER,
1193 ),1176 "Open Notebook from Server…",
1194 (1177 crate::Command::OpenFromServer(None),
1195 "open",1178 ),
1196 art::NOTEBOOK,1179 ]
1197 "Open Existing",1180 .into_iter()
1198 crate::Command::OpenNotebook,1181 .chain(crate::guide::OFFERED.then_some((
1199 ),1182 "guide",
1200 (1183 art::PAGE,
1201 "server",1184 "Open the Snowbound Guide",
1202 art::SERVER,1185 crate::Command::OpenGuide,
1203 "Open Notebook from Server…",1186 )))
1204 crate::Command::OpenFromServer(None),1187 .chain(install)
1205 ),1188 .collect(),
1206 ])
1207 .chain(crate::guide::OFFERED.then_some((
1208 "guide",
1209 art::PAGE,
1210 "Open the Snowbound Guide",
1211 crate::Command::OpenGuide,
1212 )))
1213 .chain(install)
1214 .collect(),
1215 );1189 );
1216 }1190 }
12171191
tools/canvas/README.md+1-1
...@@ -50,7 +50,7 @@ snowbound title bar · toolbar · section frame · page tabs · the page as a...@@ -50,7 +50,7 @@ snowbound title bar · toolbar · section frame · page tabs · the page as a
5050
51`--notebook FOLDER` opens a notebook laid out as OneNote lays one out. The title bar shows the application's icon and the notebook's name; below it two rows of toolbar buttons follow the Home ribbon's Basic Text group, the first nine tags and zoom (only zoom acts yet). Readable top-level sections are tabs in the notebook's order, each leaning over the next at 45° with a faint highlight along its top, and the open one on top, casting a soft shadow and framing the page and the page tabs on the right in its colour, which shades slightly down the window; a tab that opens rises to meet the frame as its outline fades in. The frame's top corners and the page's corners are rounded concentric with the window's. The open page's tab is the page's colour and joins it with rounded inside corners; subpages are indented. The interface follows the system's light or dark appearance, and in the dark one the page's paper darkens and text left at OneNote's automatic colour turns light (except on a highlight), resolved when painting so nothing re-lays out; explicit colours, highlights, pictures and ink keep theirs, except that the background art of OneNote 2010's page templates, recognised by the picture's hash, paints from bundled vector recreations drawn for the paper. Sections take their stored colour's hue at a saturation and lightness chosen for each appearance. The section's colour is also the interface accent, easing to the next section's when another opens. The search box, New Page and the page-tab toggle sit above the page tabs. Command-F searches the page titles; Enter opens the first match and Escape clears it. A page that also changed on another computer shows Keep mine / Keep theirs above it. `--section FILE TITLE` opens one section the same way at that page. Section files must be regular files; discovery rejects symbolic links.51`--notebook FOLDER` opens a notebook laid out as OneNote lays one out. The title bar shows the application's icon and the notebook's name; below it two rows of toolbar buttons follow the Home ribbon's Basic Text group, the first nine tags and zoom (only zoom acts yet). Readable top-level sections are tabs in the notebook's order, each leaning over the next at 45° with a faint highlight along its top, and the open one on top, casting a soft shadow and framing the page and the page tabs on the right in its colour, which shades slightly down the window; a tab that opens rises to meet the frame as its outline fades in. The frame's top corners and the page's corners are rounded concentric with the window's. The open page's tab is the page's colour and joins it with rounded inside corners; subpages are indented. The interface follows the system's light or dark appearance, and in the dark one the page's paper darkens and text left at OneNote's automatic colour turns light (except on a highlight), resolved when painting so nothing re-lays out; explicit colours, highlights, pictures and ink keep theirs, except that the background art of OneNote 2010's page templates, recognised by the picture's hash, paints from bundled vector recreations drawn for the paper. Sections take their stored colour's hue at a saturation and lightness chosen for each appearance. The section's colour is also the interface accent, easing to the next section's when another opens. The search box, New Page and the page-tab toggle sit above the page tabs. Command-F searches the page titles; Enter opens the first match and Escape clears it. A page that also changed on another computer shows Keep mine / Keep theirs above it. `--section FILE TITLE` opens one section the same way at that page. Section files must be regular files; discovery rejects symbolic links.
5252
53Launched without a file, the app reopens the notebooks open when it last closed, at the one shown last, or shows "No notebooks open" with New Notebook and Open Existing. Settings (the open notebooks, the sidebar, recent fonts and the user name edits are stored under, which Settings… (⌘,; Options in the sidebar and Ctrl+, on Linux) changes and which starts as the account's full name) are JSON in `~/Library/Application Support/Snowbound/settings.json` or `$XDG_CONFIG_HOME/snowbound`; `--settings FILE` uses another file, `--notebook FOLDER` adds a notebook to them, and `--screenshot` never writes them. Open Existing takes a notebook folder, its `Open Notebook.onetoc2` or a section file, which opens in the notebook its folders make up. A notebook on a mounted SMB share opens through the embedded SMB client, signed in with the password the system keychain holds, and through the mount only when that fails. The notebook button left of the section tabs opens OneNote 2010's navigation bar: the open notebooks with their sections and section groups, the open section marked; dragging a row reorders a folder or, onto a group, moves into it, and right-clicking a row, a section tab or a page tab offers OneNote's commands (Rename, Delete to the notebook's recycle bin, New Section, New Section Group, Close This Notebook; Delete, New Page, Make Subpage, Promote Subpage). Page tabs drag to reorder. The `+` above the page tabs adds a page at the end of the section, titled with its date and time as OneNote titles one, and puts the caret in its title; until its body is typed in, drawn on or clicked, templates offer our recreations of OneNote 2010's art (Ivy first), its page colours and Informal Meeting Notes' content, written as ops (`corpus/notebook-management`).53Launched without a file, the app reopens the notebooks open when it last closed, at the one shown last, or shows "No notebooks open" with New Notebook and Open Existing. Settings (the open notebooks, the sidebar, recent fonts and the user name edits are stored under, which Settings… (⌘,, Ctrl+, on Linux, or Options in the sidebar) changes and which starts as the account's full name) are JSON in `~/Library/Application Support/Snowbound/settings.json` or `$XDG_CONFIG_HOME/snowbound`; `--settings FILE` uses another file, `--notebook FOLDER` adds a notebook to them, and `--screenshot` never writes them. Open Existing takes a notebook folder, its `Open Notebook.onetoc2` or a section file, which opens in the notebook its folders make up. A notebook on a mounted SMB share opens through the embedded SMB client, signed in with the password the system keychain holds, and through the mount only when that fails. The notebook button left of the section tabs opens OneNote 2010's navigation bar: the open notebooks with their sections and section groups, the open section marked; dragging a row reorders a folder or, onto a group, moves into it, and right-clicking a row, a section tab or a page tab offers OneNote's commands (Rename, Delete to the notebook's recycle bin, New Section, New Section Group, Close This Notebook; Delete, New Page, Make Subpage, Promote Subpage). Page tabs drag to reorder. The `+` above the page tabs adds a page at the end of the section, titled with its date and time as OneNote titles one, and puts the caret in its title; until its body is typed in, drawn on or clicked, templates offer our recreations of OneNote 2010's art (Ivy first), its page colours and Informal Meeting Notes' content, written as ops (`corpus/notebook-management`).
5454
55As in OneNote 2010, Notebook Recycle Bin (File menu, a notebook's context menu, or the palette) shows the notebook's recycle bin as its own row of tabs, read-only under a bar: a page's context menu restores it to a section, keeping its identity, copies it out, or deletes it for good, a section's restores it to a folder, and Empty Recycle Bin deletes what it holds (`corpus/recycle-bin-view`). Pages another author changed since they were viewed are bold in the page list, and so are their section's tab and row and the groups and notebook holding them, until left or marked read (Mark as Read, Ctrl+Q off macOS; Mark Notebook as Read; Next Unread; Show Unread Changes in This Notebook); what was read is kept in the cache folder's `read.json`, as OneNote keeps it in its cache. Save As (⇧⌘S) writes the page or section as a OneNote section, the notebook as a OneNote package, or any of them as a PDF; opening a `.onepkg` asks, as Unpack Notebook does, for its name, colour and folder (`corpus/notebook-package`).55As in OneNote 2010, Notebook Recycle Bin (File menu, a notebook's context menu, or the palette) shows the notebook's recycle bin as its own row of tabs, read-only under a bar: a page's context menu restores it to a section, keeping its identity, copies it out, or deletes it for good, a section's restores it to a folder, and Empty Recycle Bin deletes what it holds (`corpus/recycle-bin-view`). Pages another author changed since they were viewed are bold in the page list, and so are their section's tab and row and the groups and notebook holding them, until left or marked read (Mark as Read, Ctrl+Q off macOS; Mark Notebook as Read; Next Unread; Show Unread Changes in This Notebook); what was read is kept in the cache folder's `read.json`, as OneNote keeps it in its cache. Save As (⇧⌘S) writes the page or section as a OneNote section, the notebook as a OneNote package, or any of them as a PDF; opening a `.onepkg` asks, as Unpack Notebook does, for its name, colour and folder (`corpus/notebook-package`).
5656