use std::path::PathBuf;
use std::process::ExitCode;
use clap::Parser;
use coding_tools::explain::Format;
use coding_tools::pulse::{self, HeartbeatOpts, PulseState};
use coding_tools::view::{expand_and_merge, parse_range, segments};
use coding_tools::{block, pattern, payload};
use serde_json::json;
const EXPLAIN_MD: &str = include_str!("../../docs/explain/ct-view.md");
const EXPLAIN_JSON: &str = include_str!("../../docs/explain/ct-view.json");
#[derive(Parser, Debug)]
#[command(
name = "ct-view",
version,
about = "Show a file's lines by range, or the regions around a pattern with context.",
long_about = "ct-view is a focused, bounded reader for a single file (also reachable as \
`ct view`): print a line range with --range, or the windows around a \
--match pattern with --context lines, rather than dumping the whole file. \
See `ct-view --explain` for agent-oriented documentation."
)]
struct Cli {
path: PathBuf,
#[arg(long)]
range: Option<String>,
#[arg(long = "match")]
pattern: Option<String>,
#[arg(long, value_enum)]
mode: Option<pattern::Mode>,
#[arg(long, short = 'C', default_value_t = 2)]
context: usize,
#[arg(long)]
limit: Option<usize>,
#[arg(long, value_name = "SECS")]
timeout: Option<f64>,
#[command(flatten)]
heartbeat: HeartbeatOpts,
#[arg(long)]
plain: bool,
#[arg(long)]
json: bool,
#[arg(long, value_enum, num_args = 0..=1, default_missing_value = "md")]
explain: Option<Format>,
}
fn run(cli: Cli) -> Result<ExitCode, String> {
let _watchdog = pulse::watchdog("ct-view", cli.timeout)?;
let _pulse = cli.heartbeat.start("ct-view", PulseState::new())?;
let content = std::fs::read_to_string(&cli.path)
.map_err(|e| format!("read {}: {e}", cli.path.display()))?;
let lines: Vec<&str> = content.lines().collect();
let total = lines.len();
let (mut selected, matched): (Vec<usize>, Option<bool>) = if let Some(p) = &cli.pattern {
let resolved = payload::resolve(p)?;
let pat_lines = payload::to_lines(&resolved.text);
let hits: Vec<usize> = if pat_lines.len() > 1 {
if matches!(cli.mode, Some(pattern::Mode::Glob) | Some(pattern::Mode::Regex)) {
return Err(
"a multi-line pattern matches as a literal block; --mode glob/regex is reserved"
.to_string(),
);
}
let starts = block::find_starts(&lines, &pat_lines);
if starts.is_empty()
&& let Some(m) = block::nearest_miss(&lines, &pat_lines)
{
eprintln!(
"ct-view: nearest miss: {}:{}: block diverges at its line {}",
cli.path.display(),
m.line,
m.first_diverging_line
);
eprintln!("ct-view: expected: {}", m.expected);
eprintln!("ct-view: found: {}", m.found);
}
starts
.iter()
.flat_map(|&s| s..s + pat_lines.len())
.collect()
} else {
let effective = cli
.mode
.or(resolved.from_file.then_some(pattern::Mode::Literal));
let single = pat_lines.into_iter().next().unwrap_or_default();
let re = pattern::compile_with(&single, effective)
.map_err(|e| format!("invalid --match pattern: {e}"))?;
lines
.iter()
.enumerate()
.filter(|(_, l)| re.is_match(l))
.map(|(i, _)| i)
.collect()
};
let found = !hits.is_empty();
(expand_and_merge(&hits, cli.context, total), Some(found))
} else if let Some(r) = &cli.range {
let sel = match parse_range(r, total)? {
Some((s, e)) => (s..=e).collect(),
None => Vec::new(),
};
(sel, None)
} else {
((0..total).collect(), None)
};
if let Some(limit) = cli.limit {
selected.truncate(limit);
}
if cli.json {
let out_lines: Vec<_> = selected
.iter()
.map(|&i| json!({ "n": i + 1, "text": lines[i] }))
.collect();
let mut obj = json!({
"tool": "ct-view",
"path": cli.path.display().to_string(),
"total_lines": total,
"shown": selected.len(),
"lines": out_lines,
});
if let Some(found) = matched {
obj["matched"] = json!(found);
}
println!("{obj}");
} else {
let width = total.max(1).to_string().len();
for (gi, (s, e)) in segments(&selected).iter().enumerate() {
if gi > 0 {
println!("--");
}
for (offset, line) in lines[*s..=*e].iter().enumerate() {
let n = *s + offset + 1;
if cli.plain {
println!("{line}");
} else {
println!("{n:>width$} {line}");
}
}
}
}
Ok(match matched {
Some(false) => ExitCode::from(1),
_ => ExitCode::SUCCESS,
})
}
fn main() -> ExitCode {
let cli = Cli::parse();
if let Some(fmt) = cli.explain {
let body = match fmt {
Format::Md => EXPLAIN_MD,
Format::Json => EXPLAIN_JSON,
};
print!("{body}");
return ExitCode::SUCCESS;
}
match run(cli) {
Ok(code) => code,
Err(msg) => {
eprintln!("ct-view: {msg}");
ExitCode::from(2)
}
}
}