mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(cli): name a mistyped subcommand instead of offering it to the GUI
`tty7 tree` answered "launching the GUI is not wired up yet (would open tree)". Bare `tty7 [PATH]` opens the GUI, so any word clap has no subcommand for lands in that positional — every typo came back as an offer to open a directory named after it. A word with no separator, no leading ./~, and no matching file on disk is a mistyped verb, and is now reported as one. Real paths still reach the launcher and fail there for the honest reason. Also: `ws`'s help said "the named session trees", missed by the session -> shell pass, and long_about still advertised $TTY7_SOCKET, which no longer exists — it is $TTY7_CONFIG_DIR now.
This commit is contained in:
@@ -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),
|
||||
|
||||
|
||||
@@ -42,7 +42,18 @@ pub fn execute(cli: Cli, ctx: &Context, backend: &mut dyn Backend) -> Result<Out
|
||||
let json_mode = cli.json;
|
||||
let machine = cli.machine.clone();
|
||||
match cli.command {
|
||||
None => 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<String>) -> Result<Outcome> {
|
||||
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 [
|
||||
|
||||
@@ -4481,61 +4481,6 @@ impl Tty7App {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_session_model(&self, cx: &mut Context<Self>) -> 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<Self>) -> 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()
|
||||
|
||||
Reference in New Issue
Block a user