use crate::{core, tools};
pub(in crate::cli::dispatch) fn cmd_gain(rest: &[String]) {
core::version_check::check_background();
if rest.iter().any(|a| a == "--reset") {
core::stats::reset_all();
println!("Stats reset. All token savings data cleared.");
return;
}
if rest.iter().any(|a| a == "--live") {
core::stats::gain_live();
return;
}
let model = rest.iter().enumerate().find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--model=") {
return Some(v.to_string());
}
if a == "--model" {
return rest.get(i + 1).cloned();
}
None
});
let period = rest
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--period=") {
return Some(v.to_string());
}
if a == "--period" {
return rest.get(i + 1).cloned();
}
None
})
.unwrap_or_else(|| "all".to_string());
let limit = rest
.iter()
.enumerate()
.find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--limit=") {
return v.parse::<usize>().ok();
}
if a == "--limit" {
return rest.get(i + 1).and_then(|v| v.parse::<usize>().ok());
}
None
})
.unwrap_or(10);
let copy = rest.iter().any(|a| a == "--copy");
let open = rest.iter().any(|a| a == "--open");
if rest.iter().any(|a| a == "--no-cache-adjust") {
core::context_overhead::set_no_cache_adjust(true);
}
if let Some(req) = unpublish_request(rest) {
let id = match &req {
UnpublishReq::Id(s) => Some(s.as_str()),
UnpublishReq::Latest => None,
};
crate::cli::wrapped_publish::unpublish(id);
return;
}
if rest.iter().any(|a| a == "--publish") {
let leaderboard = rest.iter().any(|a| a == "--leaderboard");
crate::cli::wrapped_publish::publish(&period, name_arg(rest).as_deref(), leaderboard);
return;
}
if let Some(req) = link_request(rest) {
let code = match &req {
LinkReq::Complete(c) => Some(c.as_str()),
LinkReq::Start => None,
};
crate::cli::wrapped_publish::link(code);
return;
}
if let Some(svg_path) = svg_target(rest) {
let report = core::wrapped::WrappedReport::generate(&period);
match std::fs::write(&svg_path, report.to_svg()) {
Ok(()) => println!(
"Wrapped card written to {svg_path}\n\
Share it directly, open it in a browser, or convert to PNG with any SVG tool."
),
Err(e) => {
eprintln!("Failed to write {svg_path}: {e}");
std::process::exit(1);
}
}
share_side_effects(&report, Some(&svg_path), copy, open);
return;
}
if let Some(html_path) = share_target(rest) {
let report = core::wrapped::WrappedReport::generate(&period);
let base = base_url_arg(rest);
match std::fs::write(&html_path, report.to_share_html(base.as_deref())) {
Ok(()) => {
println!("Share page written to {html_path}");
if base.is_some() {
println!(
"Host it at your base URL; for social preview cards, place a PNG \
(rasterise the SVG) at <base>/lean-ctx-wrapped.png."
);
} else {
println!(
"Self-contained (SVG embedded) — host it anywhere to get a permalink. \
Pass --base-url=https://… to emit social preview meta."
);
}
}
Err(e) => {
eprintln!("Failed to write {html_path}: {e}");
std::process::exit(1);
}
}
share_side_effects(&report, Some(&html_path), copy, open);
return;
}
if copy {
let report = core::wrapped::WrappedReport::generate(&period);
share_side_effects(&report, None, true, false);
return;
}
if rest.iter().any(|a| a == "--by-tool") {
cmd_by_tool(rest, limit);
} else if rest.iter().any(|a| a == "--graph") {
println!("{}", core::stats::format_gain_graph());
} else if rest.iter().any(|a| a == "--daily") {
println!("{}", core::stats::format_gain_daily());
} else if rest.iter().any(|a| a == "--json") {
println!(
"{}",
tools::ctx_gain::handle("json", Some(&period), model.as_deref(), Some(limit))
);
} else if rest.iter().any(|a| a == "--score") {
println!(
"{}",
tools::ctx_gain::handle("score", None, model.as_deref(), Some(limit))
);
} else if rest.iter().any(|a| a == "--cost") {
println!(
"{}",
tools::ctx_gain::handle("cost", None, model.as_deref(), Some(limit))
);
print_measured_spend_hint();
} else if rest.iter().any(|a| a == "--tasks") {
println!(
"{}",
tools::ctx_gain::handle("tasks", None, None, Some(limit))
);
} else if rest.iter().any(|a| a == "--agents") {
println!(
"{}",
tools::ctx_gain::handle("agents", None, None, Some(limit))
);
} else if rest.iter().any(|a| a == "--heatmap") {
println!(
"{}",
tools::ctx_gain::handle("heatmap", None, None, Some(limit))
);
} else if rest.iter().any(|a| a == "--wrapped") {
println!(
"{}",
tools::ctx_gain::handle("wrapped", Some(&period), model.as_deref(), Some(limit))
);
if !rest.iter().any(|a| a == "--publish")
&& std::io::IsTerminal::is_terminal(&std::io::stdin())
&& !crate::cli::wrapped_publish::has_published()
{
eprint!("\n Publish this card? [y/N/leaderboard] ");
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let answer = input.trim().to_lowercase();
match answer.as_str() {
"y" | "yes" => {
let cfg = crate::core::config::Config::load();
crate::cli::wrapped_publish::publish(
&period,
cfg.gain.display_name.as_deref(),
cfg.gain.leaderboard,
);
}
"l" | "leaderboard" => {
let cfg = crate::core::config::Config::load();
crate::cli::wrapped_publish::publish(
&period,
cfg.gain.display_name.as_deref(),
true,
);
}
_ => {}
}
}
} else {
crate::cli::wrapped_publish::maybe_auto_publish(&period);
}
} else if rest.iter().any(|a| a == "--pipeline") {
let stats_path = crate::core::paths::state_dir()
.unwrap_or_else(|_| dirs::home_dir().unwrap_or_default().join(".lean-ctx"))
.join("pipeline_stats.json");
if let Ok(data) = std::fs::read_to_string(&stats_path) {
if let Ok(stats) = serde_json::from_str::<core::pipeline::PipelineStats>(&data) {
println!("{}", stats.format_summary());
} else {
println!("No pipeline stats available yet (corrupt data).");
}
} else {
println!("No pipeline stats available yet. Use MCP tools to generate data.");
}
} else if rest.iter().any(|a| a == "--deep") {
println!("{}", core::stats::format_gain_body());
print!(
"{}",
tools::ctx_gain::format_deep_themed(model.as_deref(), limit)
);
println!("{}", core::stats::format_gain_footer());
} else if rest.iter().any(|a| a == "--opportunity") {
cmd_opportunity();
} else if rest.iter().any(|a| a == "--raw") {
cmd_stats_raw(rest);
} else {
println!("{}", core::stats::format_gain_hero());
print_solution_intelligence();
if let Some(banner) = core::version_check::get_update_banner() {
println!("\n{banner}");
}
print_support_hint();
print_cloud_sync_hint();
print_bridge_warning();
print_split_hint();
crate::cli::wrapped_publish::maybe_auto_publish(&period);
print_community_hint();
}
}
fn print_solution_intelligence() {
if !core::config::Config::load().solution.enabled {
return;
}
println!(
"\nSolution Intelligence: {}",
core::solution_tracker::gain_summary()
);
let solution = core::solution_tracker::snapshot();
let output_saved = solution
.output_tokens_baseline
.saturating_sub(solution.output_tokens_actual);
let loc_delta = match solution.loc_net_saved {
0 => "0".to_string(),
saved if saved > 0 => format!("-{}", format_thousands(saved.unsigned_abs())),
added => format!("+{}", format_thousands(added.unsigned_abs())),
};
let mut decisions: Vec<_> = solution.decisions_by_kind.iter().collect();
decisions.sort_unstable_by_key(|(k, _)| (*k).clone());
let breakdown = if decisions.is_empty() {
"none".to_string()
} else {
decisions
.into_iter()
.map(|(kind, count)| format!("{kind}: {}", format_thousands(*count)))
.collect::<Vec<_>>()
.join(", ")
};
let token_savings = core::savings_ledger::summary().net_saved_tokens();
let token_baseline = core::stats::load_for_display().total_input_tokens;
let combined_saved = token_savings.saturating_add(output_saved);
let combined_baseline = token_baseline.saturating_add(solution.output_tokens_baseline);
let combined_reduction = if combined_baseline == 0 {
0.0
} else {
combined_saved as f64 * 100.0 / combined_baseline as f64
};
println!(
"\nSolution Efficiency: {} output tokens saved ({}% reduction)",
format_thousands(output_saved),
solution.output_reduction_pct,
);
println!(
" LOC: {loc_delta} net lines | Decisions: {} ({breakdown})",
format_thousands(solution.decisions_total),
);
println!(
"Combined: {} total tokens saved ({combined_reduction:.1}% total reduction)",
format_thousands(combined_saved),
);
}
fn format_thousands(value: u64) -> String {
let digits = value.to_string();
let mut formatted = String::with_capacity(digits.len() + digits.len() / 3);
for (index, digit) in digits.chars().enumerate() {
if index > 0 && (digits.len() - index).is_multiple_of(3) {
formatted.push(',');
}
formatted.push(digit);
}
formatted
}
fn print_cloud_sync_hint() {
if crate::cloud_client::is_logged_in() {
let plan = crate::cloud_client::resolve_effective_plan_cached();
if plan.plan == crate::core::billing::Plan::Free {
let facts = count_local_knowledge_facts();
if facts > 0 {
eprintln!(
"\n \x1b[2m\u{1f517} {facts} knowledge facts \u{2014} local only.\n Back them up + sync across devices: lean-ctx cloud upgrade --plan pro\x1b[0m"
);
}
}
} else {
let facts = count_local_knowledge_facts();
let store = core::stats::load();
if store.total_input_tokens > 100_000 || facts > 10 {
eprintln!(
"\n \x1b[2m\u{1f517} Your context runs on this machine only.\n Sync it everywhere: lean-ctx cloud upgrade --plan pro ($9/mo)\x1b[0m"
);
}
}
}
fn count_local_knowledge_facts() -> usize {
let Ok(data_dir) = crate::core::paths::data_dir() else {
return 0;
};
let knowledge_dir = data_dir.join("knowledge");
if !knowledge_dir.is_dir() {
return 0;
}
std::fs::read_dir(&knowledge_dir)
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter(|e| e.path().is_dir())
.flat_map(|project| {
std::fs::read_dir(project.path())
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter(|f| f.path().extension().is_some_and(|ext| ext == "json"))
})
.count()
}
fn print_measured_spend_hint() {
if !crate::proxy::usage_meter::persisted_snapshot().is_empty() {
eprintln!(
"\n \x1b[2m💡 Measured provider spend available — run `lean-ctx spend` for the real per-model bill.\x1b[0m"
);
}
}
fn print_bridge_warning() {
use crate::core::gain::bridge_status::{BridgeEngagement, BridgeStatus};
let store = core::stats::load_for_display();
let saved = store
.total_input_tokens
.saturating_sub(store.total_output_tokens);
if saved > 0 {
return;
}
let bridge = BridgeStatus::detect();
if bridge.engagement != BridgeEngagement::Engaged {
eprintln!("\n \x1b[33m⚠ {}\x1b[0m", bridge.summary_line());
}
}
fn print_split_hint() {
if std::env::var_os("LEAN_CTX_DATA_DIR").is_some() {
return;
}
let dirs = core::data_dir::all_data_dirs_with_stats();
if dirs.len() >= 2 {
eprintln!(
"\n \x1b[2m💡 Stats span {} data dirs (summed above). Consolidate them with `lean-ctx doctor`.\x1b[0m",
dirs.len()
);
}
}
fn print_community_hint() {
let s = core::savings_ledger::summary();
if s.total_events == 0 {
return;
}
let on_board = crate::cli::wrapped_publish::has_leaderboard_entry();
let published = crate::cli::wrapped_publish::has_published();
let has_name = crate::core::config::Config::load()
.gain
.display_name
.is_some();
let body = if on_board && has_name {
"💡 On the public leaderboard (https://leanctx.com/metrics).\n \
Refresh: lean-ctx gain --publish --leaderboard\n \
Multiple machines? lean-ctx gain --link (combine into one entry)\n \
Remove: lean-ctx gain --unpublish"
.to_string()
} else if on_board {
"💡 You're on the leaderboard as \"anonymous\". Claim your handle:\n \
lean-ctx gain --publish --leaderboard --name=\"your handle\"\n \
Multiple machines? lean-ctx gain --link (combine into one entry)"
.to_string()
} else if published {
"💡 You're published privately. List on the public leaderboard at https://leanctx.com/metrics:\n \
lean-ctx gain --publish --leaderboard --name=\"your handle\" · Remove: lean-ctx gain --unpublish"
.to_string()
} else {
"💡 Join the public leaderboard at https://leanctx.com/metrics (opt-in — shares only 4 aggregate totals, never your code):\n \
Publish: lean-ctx gain --publish --leaderboard --name=\"your handle\" · Remove: lean-ctx gain --unpublish"
.to_string()
};
eprintln!("\n \x1b[2m{body}\x1b[0m");
}
fn print_support_hint() {
use crate::core::theme;
let t = theme::load_theme(&crate::core::config::Config::load().theme);
let rst = theme::rst();
let bold = theme::bold();
let dim = theme::dim();
let acc = t.accent.fg();
let heart = t.danger.fg();
let w = 57;
let side = t.box_side();
let row = |content: &str| -> String {
let padded = theme::pad_right(content, w);
format!(" {side}{padded}{side}")
};
println!();
println!(" {}", t.box_top(w));
println!(
"{}",
row(&format!(
" {heart}\u{2665}{rst} {bold}Enjoying lean-ctx? Keep it free & independent.{rst}"
))
);
println!("{}", row(""));
println!(
"{}",
row(&format!(
" {dim}Back development for the price of a coffee \u{2192}{rst}"
))
);
println!(
"{}",
row(&format!(" {acc}{bold}https://leanctx.com/support{rst}"))
);
println!(" {}", t.box_bottom(w));
}
enum UnpublishReq {
Latest,
Id(String),
}
fn unpublish_request(rest: &[String]) -> Option<UnpublishReq> {
for a in rest {
if let Some(v) = a.strip_prefix("--unpublish=") {
return Some(UnpublishReq::Id(v.to_string()));
}
if a == "--unpublish" {
return Some(UnpublishReq::Latest);
}
}
None
}
enum LinkReq {
Start,
Complete(String),
}
fn link_request(rest: &[String]) -> Option<LinkReq> {
for (i, a) in rest.iter().enumerate() {
if let Some(v) = a.strip_prefix("--link=") {
return Some(LinkReq::Complete(v.to_string()));
}
if a == "--link" {
return Some(match rest.get(i + 1) {
Some(next) if !next.starts_with('-') => LinkReq::Complete(next.clone()),
_ => LinkReq::Start,
});
}
}
None
}
fn name_arg(rest: &[String]) -> Option<String> {
rest.iter().enumerate().find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--name=") {
return Some(v.to_string());
}
if a == "--name" {
return rest.get(i + 1).cloned();
}
None
})
}
fn svg_target(rest: &[String]) -> Option<String> {
let mut requested = false;
let mut path: Option<String> = None;
for (i, a) in rest.iter().enumerate() {
if let Some(v) = a
.strip_prefix("--svg=")
.or_else(|| a.strip_prefix("--card="))
{
requested = true;
path = Some(v.to_string());
} else if a == "--svg" || a == "--card" {
requested = true;
if let Some(next) = rest.get(i + 1)
&& !next.starts_with('-')
{
path = Some(next.clone());
}
}
}
requested.then(|| path.unwrap_or_else(|| "lean-ctx-wrapped.svg".to_string()))
}
fn share_target(rest: &[String]) -> Option<String> {
let mut requested = false;
let mut path: Option<String> = None;
for (i, a) in rest.iter().enumerate() {
if let Some(v) = a
.strip_prefix("--share=")
.or_else(|| a.strip_prefix("--page="))
{
requested = true;
path = Some(v.to_string());
} else if a == "--share" || a == "--page" {
requested = true;
if let Some(next) = rest.get(i + 1)
&& !next.starts_with('-')
{
path = Some(next.clone());
}
}
}
requested.then(|| path.unwrap_or_else(|| "lean-ctx-wrapped.html".to_string()))
}
fn base_url_arg(rest: &[String]) -> Option<String> {
rest.iter().enumerate().find_map(|(i, a)| {
if let Some(v) = a.strip_prefix("--base-url=") {
return Some(v.to_string());
}
if a == "--base-url" {
return rest.get(i + 1).cloned();
}
None
})
}
fn share_side_effects(
report: &core::wrapped::WrappedReport,
path: Option<&str>,
copy: bool,
open: bool,
) {
if copy {
let text = report.share_text(None);
if core::share::copy_to_clipboard(&text) {
println!("Copied to clipboard: {text}");
} else {
println!("Share text (copy it): {text}");
}
}
if open && let Some(p) = path {
if core::share::open_in_browser(p) {
println!("Opened {p}");
} else {
println!("Could not open {p} automatically — open it manually.");
}
}
}
fn cmd_opportunity() {
use crate::core::theme;
let t = {
let cfg = crate::core::config::Config::load();
theme::load_theme(&cfg.theme)
};
let rst = theme::rst();
let bold = theme::bold();
let dim = theme::dim();
let w = 57;
let history = crate::cli::common::load_shell_history();
let result = crate::tools::ctx_discover::analyze_history(&history, 10);
let store = core::stats::load_for_display();
let optimized_cmds = store.total_commands;
if result.missed_commands.is_empty() {
println!(
"\n {sc}{bold}All compressible commands are already using lean-ctx!{rst}\n {dim}{optimized_cmds} commands optimized.{rst}\n",
sc = t.success.fg(),
);
return;
}
let total_missed: u64 = result
.missed_commands
.iter()
.map(|c| c.estimated_tokens as u64)
.sum();
let total_count: u64 = result.missed_commands.iter().map(|c| c.count as u64).sum();
println!();
println!(" {}", t.box_top(w));
let side = t.box_side();
let padded = |s: &str| -> String {
let p = theme::pad_right(s, w);
format!(" {side}{p}{side}")
};
println!(
"{}",
padded(&format!(" {bold}{}Opportunity Report{rst}", t.accent.fg()))
);
println!("{}", padded(""));
println!(
"{}",
padded(&format!(
" {bold}~{}{rst} {dim}tokens/month going uncompressed{rst}",
crate::core::wrapped::format_tokens(total_missed),
))
);
println!("{}", padded(""));
for entry in result.missed_commands.iter().take(8) {
let line = format!(
" {dim}{:<12}{rst} {:>4}x {bold}~{}{rst} {dim}tokens recoverable{rst}",
entry.prefix,
entry.count,
crate::core::wrapped::format_tokens(entry.estimated_tokens as u64),
);
println!("{}", padded(&line));
}
println!("{}", padded(""));
let opt_line = format!(
" {dim}Already optimized: {optimized_cmds} commands ({pct}%){rst}",
pct = total_count
.checked_add(optimized_cmds)
.and_then(|total| optimized_cmds.checked_mul(100)?.checked_div(total))
.unwrap_or(100)
);
println!("{}", padded(&opt_line));
println!(" {}", t.box_bottom(w));
println!();
let sec = t.secondary.fg();
println!(" {sec}lean-ctx init --global{rst} {dim}Compress everything{rst}");
println!();
}
fn cmd_by_tool(rest: &[String], limit: usize) {
use std::collections::HashMap;
#[derive(Default)]
struct Agg {
calls: u64,
saved_tokens: u64,
saved_usd: f64,
baseline_tokens: u64,
}
let mut by_tool: HashMap<String, Agg> = HashMap::new();
for e in core::savings_ledger::all_events() {
let a = by_tool.entry(e.tool).or_default();
a.calls += 1;
a.saved_tokens += e.saved_tokens;
a.saved_usd += e.saved_usd;
a.baseline_tokens += e.baseline_tokens;
}
let mut rows: Vec<(String, Agg)> = by_tool.into_iter().collect();
rows.sort_by(|a, b| {
b.1.saved_tokens
.cmp(&a.1.saved_tokens)
.then_with(|| a.0.cmp(&b.0))
});
rows.truncate(limit);
let pct = |a: &Agg| -> f64 {
if a.baseline_tokens > 0 {
a.saved_tokens as f64 / a.baseline_tokens as f64 * 100.0
} else {
0.0
}
};
if rest.iter().any(|a| a == "--json") {
let arr: Vec<_> = rows
.iter()
.map(|(tool, a)| {
serde_json::json!({
"tool": tool,
"calls": a.calls,
"saved_tokens": a.saved_tokens,
"saved_usd": a.saved_usd,
"baseline_tokens": a.baseline_tokens,
"saved_pct": pct(a),
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&arr).unwrap_or_else(|_| "[]".to_string())
);
return;
}
if rows.is_empty() {
println!(
"No per-tool savings recorded yet. Use lean-ctx-backed tools (ctx_read, \
ctx_search, ctx_shell) to populate the ledger."
);
return;
}
println!("Savings by tool (most saved first):\n");
println!(
" {:<16} {:>8} {:>12} {:>10} {:>7}",
"TOOL", "CALLS", "SAVED", "USD", "SAVED%"
);
for (tool, a) in &rows {
println!(
" {:<16} {:>8} {:>12} {:>10} {:>6.1}%",
tool,
a.calls,
core::wrapped::format_tokens(a.saved_tokens),
format!("${:.2}", a.saved_usd),
pct(a),
);
}
}
fn cmd_stats_raw(rest: &[String]) {
if rest.iter().any(|a| a == "--json" || a == "json") {
let store = core::stats::load();
println!(
"{}",
serde_json::to_string_pretty(&store).unwrap_or_else(|_| "{}".to_string())
);
} else {
let store = core::stats::load();
let input_saved = store
.total_input_tokens
.saturating_sub(store.total_output_tokens);
let pct = if store.total_input_tokens > 0 {
input_saved as f64 / store.total_input_tokens as f64 * 100.0
} else {
0.0
};
println!("Commands: {}", store.total_commands);
println!("Input: {} tokens", store.total_input_tokens);
println!("Output: {} tokens", store.total_output_tokens);
println!("Saved: {input_saved} tokens ({pct:.1}%)");
println!();
let sol = crate::core::solution_tracker::snapshot();
if sol.decisions_total > 0 || sol.loc_added > 0 || sol.loc_removed > 0 {
let sol_pct = sol.output_reduction_pct;
println!("Solution Efficiency:");
if sol.output_tokens_baseline > 0 {
let sol_saved = sol
.output_tokens_baseline
.saturating_sub(sol.output_tokens_actual);
println!(" Output: {sol_saved} tokens saved ({sol_pct}% reduction)");
}
println!(
" LOC: {} net lines saved | {} decisions ({} stdlib, {} reuse, {} native, {} yagni)",
sol.loc_net_saved,
sol.decisions_total,
sol.decisions_by_kind.get("stdlib").unwrap_or(&0),
sol.decisions_by_kind.get("reuse").unwrap_or(&0),
sol.decisions_by_kind.get("native").unwrap_or(&0),
sol.decisions_by_kind.get("yagni").unwrap_or(&0),
);
if input_saved > 0 || sol.output_tokens_baseline > 0 {
let combined = input_saved
+ sol
.output_tokens_baseline
.saturating_sub(sol.output_tokens_actual);
println!(" Combined: {combined} total tokens saved");
}
println!();
}
println!("CEP sessions: {}", store.cep.sessions);
println!(
"CEP tokens: {} → {}",
store.cep.total_tokens_original, store.cep.total_tokens_compressed
);
}
}
#[cfg(test)]
mod tests {
use super::{base_url_arg, share_target, svg_target};
fn args(xs: &[&str]) -> Vec<String> {
xs.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn no_flag_means_no_card() {
assert_eq!(svg_target(&args(&["--wrapped", "--period=month"])), None);
}
#[test]
fn bare_flag_uses_default_path() {
assert_eq!(
svg_target(&args(&["--svg"])).as_deref(),
Some("lean-ctx-wrapped.svg")
);
assert_eq!(
svg_target(&args(&["--card"])).as_deref(),
Some("lean-ctx-wrapped.svg")
);
}
#[test]
fn explicit_path_is_used() {
assert_eq!(
svg_target(&args(&["--svg=out.svg"])).as_deref(),
Some("out.svg")
);
assert_eq!(
svg_target(&args(&["--card=c.svg"])).as_deref(),
Some("c.svg")
);
assert_eq!(
svg_target(&args(&["--svg", "chosen.svg"])).as_deref(),
Some("chosen.svg")
);
}
#[test]
fn following_flag_is_not_consumed_as_path() {
assert_eq!(
svg_target(&args(&["--svg", "--period=week"])).as_deref(),
Some("lean-ctx-wrapped.svg")
);
}
#[test]
fn share_target_resolves_paths() {
assert_eq!(share_target(&args(&["--wrapped"])), None);
assert_eq!(
share_target(&args(&["--share"])).as_deref(),
Some("lean-ctx-wrapped.html")
);
assert_eq!(
share_target(&args(&["--page=out.html"])).as_deref(),
Some("out.html")
);
assert_eq!(
share_target(&args(&["--share", "--base-url=https://x"])).as_deref(),
Some("lean-ctx-wrapped.html"),
"a following flag must not be eaten as the path"
);
}
#[test]
fn base_url_is_parsed_both_forms() {
assert_eq!(
base_url_arg(&args(&["--base-url=https://me.dev"])).as_deref(),
Some("https://me.dev")
);
assert_eq!(
base_url_arg(&args(&["--base-url", "https://me.dev"])).as_deref(),
Some("https://me.dev")
);
assert_eq!(base_url_arg(&args(&["--share"])), None);
}
}