From 49541ff8b0f646f208c9bea2c3cef1e84d7385a1 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 17:28:30 +0800 Subject: [PATCH] fix(tree): a workspace or tab name of only spaces is no name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rename box in the GUI trims what is typed and maps an empty result to unnamed. The CLI did not: `tty7 ws new " "` stored a name of spaces, which prints as a blank cell in `ws ls` — it looks unnamed without being unnamed, so it sorts, compares and resolves as a name nobody can see or retype. The same door was open on rename, for tabs as well as workspaces. Normalising in `MachineStore` rather than in each caller settles it for both front doors at once, and for whatever is added beside them later; the GUI's own trim becomes redundant rather than wrong. Surrounding whitespace is dropped from names that survive, so `" web "` and `"web"` are the same name rather than two. --- crates/tty7-core/src/core/machine.rs | 55 +++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/tty7-core/src/core/machine.rs b/crates/tty7-core/src/core/machine.rs index e785d1ac..2e623877 100644 --- a/crates/tty7-core/src/core/machine.rs +++ b/crates/tty7-core/src/core/machine.rs @@ -506,6 +506,23 @@ fn not_found(msg: impl Into) -> io::Error { io::Error::new(io::ErrorKind::NotFound, msg.into()) } +/// A name the tree will keep: trimmed, and nothing left means unnamed. +/// +/// The rename box in the GUI trims and maps an empty result to unnamed +/// already, but the CLI handed `tty7 ws new " "` straight through, and a +/// workspace named with spaces prints as a blank cell — it looks unnamed +/// without being unnamed, so it sorts, compares and resolves as a name nobody +/// can see or retype. One rule at the store settles it for both front doors +/// and for anything added beside them later. +fn normalize_name(name: Option) -> Option { + let name = name?; + let trimmed = name.trim(); + match trimmed.is_empty() { + true => None, + false => Some(trimmed.to_string()), + } +} + impl MachineStore { /// Open the store at `path`, reading whatever is there. /// @@ -604,7 +621,7 @@ impl MachineStore { } let workspace = Workspace { id: id.unwrap_or_default(), - name: name.clone(), + name: normalize_name(name.clone()), ..Workspace::default() }; m.workspaces.push(workspace.clone()); @@ -627,6 +644,7 @@ impl MachineStore { name: Option, origin: Option, ) -> io::Result<()> { + let name = normalize_name(name); self.mutate(origin, |m| { let ws = find_workspace(m, id)?; ws.name = name.clone(); @@ -754,6 +772,7 @@ impl MachineStore { name: Option, origin: Option, ) -> io::Result<()> { + let name = normalize_name(name); self.mutate(origin, |m| { let t = find_tab(m, workspace, tab)?; t.name = name.clone(); @@ -2035,6 +2054,40 @@ mod tests { } } + #[test] + fn a_name_that_is_only_spaces_is_no_name() { + let (store, _dir) = store(); + // The GUI's rename box trims and maps empty to unnamed; `tty7 ws new + // " "` used to get a workspace whose name printed as a blank cell, + // which looks unnamed without being unnamed. + let ws = store + .workspace_create(None, Some(" ".into()), None) + .unwrap(); + assert_eq!(ws.name, None, "a name of spaces is not a name"); + + let named = store + .workspace_create(None, Some(" web ".into()), None) + .unwrap(); + assert_eq!( + named.name.as_deref(), + Some("web"), + "surrounding space is dropped" + ); + + store + .workspace_rename(named.id, Some(" \t ".into()), None) + .unwrap(); + assert_eq!(store.workspace(named.id).unwrap().name, None); + + store + .workspace_rename(named.id, Some(" api ".into()), None) + .unwrap(); + assert_eq!( + store.workspace(named.id).unwrap().name.as_deref(), + Some("api") + ); + } + #[test] fn workspace_create_rename_touch_delete_land_and_broadcast() { let (store, _dir) = store();