diff --git a/crates/tty7-cli/src/cli.rs b/crates/tty7-cli/src/cli.rs index fe4a2517..52c10084 100644 --- a/crates/tty7-cli/src/cli.rs +++ b/crates/tty7-cli/src/cli.rs @@ -12,8 +12,9 @@ use clap::{ArgGroup, Args, Parser, Subcommand}; agents: every verb is non-interactive, and --json makes the output\n\ machine-readable.\n\n\ An agent reaches the server through this binary, not through a wire\n\ - protocol of its own. Inside a tty7 shell, $TTY7_PANE / $TTY7_WS /\n\ - $TTY7_SOCKET let the address-taking verbs run with no address given." + protocol of its own. Inside a tty7 shell, $TTY7_CONFIG_DIR points at\n\ + the server, and $TTY7_PANE / $TTY7_WS let the address-taking verbs run\n\ + with no address given." )] pub struct Cli { #[arg( @@ -93,7 +94,7 @@ pub enum Command { #[command( subcommand, - about = "Workspaces: the named session trees the server keeps alive" + about = "Workspaces: the named trees of tabs and panes the server keeps alive" )] Ws(WsCmd), diff --git a/crates/tty7-cli/src/commands.rs b/crates/tty7-cli/src/commands.rs index 211865ce..10b59107 100644 --- a/crates/tty7-cli/src/commands.rs +++ b/crates/tty7-cli/src/commands.rs @@ -42,7 +42,18 @@ pub fn execute(cli: Cli, ctx: &Context, backend: &mut dyn Backend) -> Result launch_gui(cli.path), + None => match cli.path { + // clap has no subcommand for this word, so it landed in [PATH]. + // Treat a word that is not a path as the typo it almost certainly + // is: answering "launching the GUI is not wired up yet" to + // `tty7 statu` helps nobody. + Some(word) if !looks_like_a_path(&word) => bail!( + "unknown subcommand '{word}' — run `tty7 --help` for the list. \ + (A path in this position would open the GUI there, but \ + '{word}' does not name one.)" + ), + path => launch_gui(path), + }, Some(Command::Ls) | Some(Command::Ws(WsCmd::Ls)) => ws_ls(backend), Some(Command::Ws(WsCmd::Tree { ws })) => ws_tree(ws.as_deref(), ctx, backend), Some(Command::Ws(WsCmd::New { name })) => ws_new(name, backend), @@ -113,6 +124,20 @@ fn local_server( act() } +/// Whether a bare word in the `[PATH]` position was meant as a path. +/// +/// Anything with a separator, a leading `.`/`~`, or that actually exists on +/// disk counts. A plain word like `tree` or `statu` does not — it is a +/// mistyped subcommand, and saying so beats offering to open the GUI there. +fn looks_like_a_path(s: &str) -> bool { + s.starts_with('/') + || s.starts_with('.') + || s.starts_with('~') + || s.contains('/') + || s.contains('\\') + || std::path::Path::new(s).exists() +} + fn launch_gui(path: Option) -> Result { match path { Some(p) => bail!("launching the GUI is not wired up yet (would open {p})"), @@ -1403,6 +1428,25 @@ mod tests { } } + #[test] + fn a_mistyped_subcommand_is_named_as_one_not_offered_to_the_gui() { + for typo in ["tree", "statu", "pnae", "workspace"] { + let err = execute(cli(&["tty7", typo]), &Context::default(), &mut mock()) + .expect_err("a bare word is not a path"); + let msg = err.to_string(); + assert!(msg.contains("unknown subcommand"), "{typo}: {msg}"); + assert!(msg.contains(typo), "{typo}: {msg}"); + } + + // Things that do look like paths still reach the GUI launcher, and fail + // there for the honest reason. + for path in ["/tmp", "./src", "~/proj", "a/b"] { + let err = execute(cli(&["tty7", path]), &Context::default(), &mut mock()) + .expect_err("the GUI launcher is not implemented yet"); + assert!(err.to_string().contains("not wired up"), "{path}: {err}"); + } + } + #[test] fn the_still_missing_verbs_say_so_without_touching_the_wire() { for (args, needle) in [ diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 471db003..a60e086d 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -4481,61 +4481,6 @@ impl Tty7App { .into_any_element() } - fn render_session_model(&self, cx: &mut Context) -> AnyElement { - let theme = cx.theme(); - let (foreground, muted_fg) = (theme.foreground, theme.muted_foreground); - - let entry = |term: &'static str, meaning: &'static str| { - v_flex() - .gap_0p5() - .child( - div() - .text_sm() - .font_weight(FontWeight::MEDIUM) - .text_color(foreground) - .child(term), - ) - .child(div().text_xs().text_color(muted_fg).child(meaning)) - }; - - v_flex() - .mt_6() - .gap_2() - .child(self.section_rule(cx)) - .child( - div() - .text_sm() - .font_weight(FontWeight::MEDIUM) - .text_color(foreground) - .child("How shells work"), - ) - .child(div().text_xs().text_color(muted_fg).child( - "Your shells run in tty7's background server, not in this window. That is what lets them outlive a quit or a reboot — and it means \"close\" and \"end\" are different things here.", - )) - .child( - v_flex() - .mt_2() - .gap_3() - .child(entry( - "Closing a window (⌘W on the last tab)", - "Detaches the workspace. Every shell keeps running; the workspace waits on the home page and in the title-bar menu.", - )) - .child(entry( - "Quitting tty7 (⌘Q)", - "Same deal, for every window. Nothing running is interrupted.", - )) - .child(entry( - "Stop Workspace", - "Ends that workspace's shells but keeps its layout, so you can start it again with fresh ones.", - )) - .child(entry( - "Delete Workspace", - "Ends the shells and forgets the layout. The only step here you can't undo.", - )), - ) - .into_any_element() - } - fn render_settings_about(&self, cx: &mut Context) -> AnyElement { let theme = cx.theme(); let (foreground, muted_fg) = (theme.foreground, theme.muted_foreground); @@ -4599,7 +4544,6 @@ impl Tty7App { .child("Pure Rust · GPU rendering on Zed's gpui · VT core from Alacritty"), ), ) - .child(self.render_session_model(cx)) .child( v_flex() .mt_6()