use std::io::{self, IsTerminal, Write};
use anyhow::{Context, Result, bail};
use clap::{Args, Parser, Subcommand};
use crate::{
diff,
input::InputSource,
load,
profile::TypeProfile,
transform::{self, FormatKind, FormatOptions},
viewer,
};
#[derive(Debug, Parser)]
#[command(
name = "fmtview",
version,
about = "Fast formatter, diff tool, and terminal viewer for JSON, JSONL, XML-compatible markup, Markdown, TOML, plain text, and Jinja templates",
args_conflicts_with_subcommands = true,
subcommand_precedence_over_arg = true
)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[command(flatten)]
format: FormatCommand,
}
#[derive(Debug, Args)]
struct FormatCommand {
#[arg(value_name = "INPUT", default_value = "-")]
input: String,
#[arg(short = 't', long = "type", value_enum, default_value_t = FormatKind::Auto)]
kind: FormatKind,
#[arg(long, value_name = "STRING")]
literal: Option<String>,
#[arg(long, default_value_t = 2)]
indent: usize,
}
#[derive(Debug, Subcommand)]
enum Command {
Diff(DiffCommand),
}
#[derive(Debug, Args)]
struct DiffCommand {
left: String,
right: String,
#[arg(short = 't', long = "type", value_enum, default_value_t = FormatKind::Auto)]
kind: FormatKind,
#[arg(long, default_value_t = 2)]
indent: usize,
}
pub fn run() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Command::Diff(command)) => run_diff(command),
None => run_format(cli.format),
}
}
fn run_format(command: FormatCommand) -> Result<()> {
validate_indent(command.indent)?;
let input = InputSource::from_arg(&command.input, command.literal.as_deref())
.context("failed to open input")?;
let options = FormatOptions {
kind: command.kind,
indent: command.indent,
};
let profile = TypeProfile::resolve(&input, &options)?;
let resolved_options = profile.format_options(command.indent);
if should_view() {
let opened = if command.kind == FormatKind::Auto {
load::open_view_file_with_fallback(&input, &resolved_options, profile, true)?
} else {
load::open_view_file(&input, &resolved_options, profile)?
};
viewer::run(opened.file, opened.content, opened.notice)
} else {
let formatted =
transform::transform_source_to_temp(&input, &resolved_options, profile.transform)?;
copy_temp_to_stdout(&formatted)
}
}
fn run_diff(command: DiffCommand) -> Result<()> {
validate_indent(command.indent)?;
let left = InputSource::from_arg(&command.left, None).context("failed to open left input")?;
let right =
InputSource::from_arg(&command.right, None).context("failed to open right input")?;
let options = FormatOptions {
kind: command.kind,
indent: command.indent,
};
if should_view() {
let model = diff::diff_view(&left, &right, &options)?;
viewer::run_diff(model)
} else {
let diffed = diff::diff_sources(&left, &right, &options, false)?;
copy_temp_to_stdout(&diffed)
}
}
fn should_view() -> bool {
io::stdout().is_terminal()
}
fn copy_temp_to_stdout(temp: &tempfile::NamedTempFile) -> Result<()> {
let mut file = std::fs::File::open(temp.path()).context("failed to reopen formatted output")?;
let mut stdout = io::stdout().lock();
io::copy(&mut file, &mut stdout).context("failed to write formatted output to stdout")?;
stdout.flush().context("failed to flush stdout")?;
Ok(())
}
fn validate_indent(indent: usize) -> Result<()> {
if indent == 0 || indent > 16 {
bail!("--indent must be between 1 and 16");
}
Ok(())
}