use std::path::PathBuf;
use std::time::Duration;
use anyhow::{Context, Result};
use clap::Parser;
use tokio::signal;
use tokio::sync::mpsc;
use ripgrab::{FilterSet, RenderMode, Renderer, TailEvent, TailHandle};
/// Tail logs with ripgrep-style filters and structured field extraction.
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// Files to follow. At least one is required.
#[arg(required = true)]
paths: Vec<PathBuf>,
/// Include lines matching this regex (repeatable).
#[arg(short = 'm', long = "match")]
matches: Vec<String>,
/// Drop lines matching this regex (repeatable).
#[arg(short = 'v', long = "exclude")]
excludes: Vec<String>,
/// Named-capture regex for structured extraction.
#[arg(short = 'x', long = "extract")]
extract: Option<String>,
/// Skip lines older than this duration (e.g. "10m", "2h").
#[arg(long)]
since: Option<humantime::Duration>,
/// Do not follow the file after reading existing contents.
#[arg(long)]
no_follow: bool,
/// Disable ANSI color output.
#[arg(long, env = "NO_COLOR")]
no_color: bool,
/// Start with the last N lines of each file before following.
#[arg(short = 'n', long = "lines", default_value_t = 0)]
lines: usize,
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
let cli = Cli::parse();
let filters = FilterSet::compile(&cli.matches, &cli.excludes, cli.extract.as_deref())
.context("failed to compile filter rules")?;
let mode = if cli.extract.is_some() {
RenderMode::Table
} else {
RenderMode::Stream
};
let renderer = Renderer::new(mode, !cli.no_color);
let (tx, mut rx) = mpsc::channel::<TailEvent>(1024);
let mut handles = Vec::with_capacity(cli.paths.len());
for path in &cli.paths {
let handle = TailHandle::spawn(
path.clone(),
tx.clone(),
cli.lines,
cli.no_follow,
cli.since.map(Duration::from),
)
.with_context(|| format!("could not open {}", path.display()))?;
handles.push(handle);
}
drop(tx);
let mut stdout = tokio::io::stdout();
let pump = async {
while let Some(event) = rx.recv().await {
match event {
TailEvent::Line { source, line } => {
if let Some(outcome) = filters.apply(&line) {
renderer.write(&mut stdout, &source, &line, &outcome).await?;
}
}
TailEvent::Rotation { source } => {
renderer.notice(&mut stdout, &source, "file rotated").await?;
}
TailEvent::Error { source, err } => {
renderer.notice(&mut stdout, &source, &format!("{err}")).await?;
}
}
}
Ok::<_, anyhow::Error>(())
};
tokio::select! {
res = pump => {
for h in handles { h.shutdown().await; }
res
}
_ = signal::ctrl_c() => {
for h in handles { h.shutdown().await; }
Ok(())
}
}
}