diff --git a/README.md b/README.md index 460915e..be2004a 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ Select profile: Enter number (or blank to abort): 2 ``` -Choosing a number starts the TUI with that profile. Pressing Enter on blank aborts without connecting. +Choosing a number starts the TUI with that profile. A built‑in `demo` option is always appended; selecting it launches a local agent on port 3231 (no TLS) and connects to `ws://127.0.0.1:3231/ws`. Pressing Enter on blank aborts without connecting. ### JSON format diff --git a/socktop/src/main.rs b/socktop/src/main.rs index ae92554..aeb308a 100644 --- a/socktop/src/main.rs +++ b/socktop/src/main.rs @@ -17,6 +17,7 @@ struct ParsedArgs { tls_ca: Option, profile: Option, save: bool, + demo: bool, } fn parse_args>(args: I) -> Result { @@ -25,13 +26,14 @@ fn parse_args>(args: I) -> Result = None; let mut tls_ca: Option = None; let mut profile: Option = None; - let mut save = false; // --save-profile + let mut save = false; // --save + let mut demo = false; // --demo while let Some(arg) = it.next() { match arg.as_str() { "-h" | "--help" => { return Err(format!( - "Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [ws://HOST:PORT/ws]" + "Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]" )); } "--tls-ca" | "-t" => { @@ -43,6 +45,9 @@ fn parse_args>(args: I) -> Result { save = true; } + "--demo" => { + demo = true; + } _ if arg.starts_with("--tls-ca=") => { if let Some((_, v)) = arg.split_once('=') { if !v.is_empty() { @@ -62,7 +67,7 @@ fn parse_args>(args: I) -> Result>(args: I) -> Result Result<(), Box> { } }; + // Demo mode short-circuit (ignore other args except conflicting ones) + if parsed.demo || matches!(parsed.profile.as_deref(), Some("demo")) { + return run_demo_mode(parsed.tls_ca.as_deref()).await; + } + let profiles_file = load_profiles(); let req = ProfileRequest { profile_name: parsed.profile.clone(), @@ -141,7 +152,9 @@ async fn main() -> Result<(), Box> { (u, t) } ResolveProfile::Loaded(u, t) => (u, t), - ResolveProfile::PromptSelect(names) => { + ResolveProfile::PromptSelect(mut names) => { + // Always add demo option to list + if !names.iter().any(|n| n == "demo") { names.push("demo".into()); } eprintln!("Select profile:"); for (i, n) in names.iter().enumerate() { eprintln!(" {}. {}", i + 1, n); @@ -153,6 +166,7 @@ async fn main() -> Result<(), Box> { if let Ok(idx) = line.trim().parse::() { if idx >= 1 && idx <= names.len() { let name = &names[idx - 1]; + if name == "demo" { return run_demo_mode(parsed.tls_ca.as_deref()).await; } if let Some(entry) = profiles_mut.profiles.get(name) { (entry.url.clone(), entry.tls_ca.clone()) } else { @@ -218,3 +232,53 @@ fn prompt_string(prompt: &str) -> io::Result { io::stdin().read_line(&mut line)?; Ok(line) } + +// --- Demo Mode --- + +async fn run_demo_mode(_tls_ca: Option<&str>) -> Result<(), Box> { + let port = 3231; + let url = format!("ws://127.0.0.1:{port}/ws"); + let child = spawn_demo_agent(port)?; + // Use select to handle Ctrl-C and normal quit + let mut app = App::new(); + tokio::select! { + res = app.run(&url, None) => { drop(child); res } + _ = tokio::signal::ctrl_c() => { + // Drop child (kills agent) then return + drop(child); + Ok(()) + } + } +} + +struct DemoGuard(std::sync::Arc>>); +impl Drop for DemoGuard { fn drop(&mut self) { if let Some(mut ch) = self.0.lock().unwrap().take() { let _ = ch.kill(); } } } + +fn spawn_demo_agent(port: u16) -> Result> { + let candidate = find_agent_executable(); + let mut cmd = std::process::Command::new(candidate); + cmd.arg("--port").arg(port.to_string()); + cmd.env("SOCKTOP_ENABLE_SSL", "0"); + cmd.env("SOCKTOP_AGENT_GPU", "0"); + cmd.env("SOCKTOP_AGENT_TEMP", "0"); + let child = cmd.spawn()?; + // Give the agent a brief moment to start + std::thread::sleep(std::time::Duration::from_millis(300)); + Ok(DemoGuard(std::sync::Arc::new(std::sync::Mutex::new(Some(child))))) +} + +fn find_agent_executable() -> std::path::PathBuf { + let self_exe = std::env::current_exe().ok(); + if let Some(exe) = self_exe { + if let Some(parent) = exe.parent() { + #[cfg(windows)] + let name = "socktop_agent.exe"; + #[cfg(not(windows))] + let name = "socktop_agent"; + let candidate = parent.join(name); + if candidate.exists() { return candidate; } + } + } + // Fallback to relying on PATH + std::path::PathBuf::from("socktop_agent") +}