use std::io::Write;
use crate::cli::BrowseArgs;
use crate::client::{self, Envelope};
pub fn run(args: BrowseArgs) -> Result<(), Box<dyn std::error::Error>> {
let fmt_id = resolve_format(&args.format);
if !args.oneshot && client::daemon_alive(args.port) {
browse_via_daemon(&args, fmt_id)
} else {
browse_oneshot(&args, fmt_id)
}
}
fn browse_via_daemon(args: &BrowseArgs, fmt: u8) -> Result<(), Box<dyn std::error::Error>> {
let url = format!("http://127.0.0.1:{}/api/navigate", args.port);
let body = serde_json::json!({"url": args.url, "format": fmt});
let client = reqwest::blocking::Client::new();
let resp = client.post(&url).json(&body).send()?;
let env: Envelope = resp.json()?;
if !env.success {
let err = env.error.unwrap_or_else(|| "request failed".into());
eprintln!("API error: {err}");
return browse_oneshot(args, fmt);
}
let output = format_envelope(&env.data, fmt);
write_output(&output, args.output.as_deref())
}
fn browse_oneshot(args: &BrowseArgs, fmt: u8) -> Result<(), Box<dyn std::error::Error>> {
let resolved = nomad_core::omnibox::resolve(&args.url);
let client = nomad_core::net::build_blocking_client()
.ok_or_else(|| "failed to init HTTP client (no TLS?)")?;
let resp = client.get(&resolved).send()?;
let body = resp.bytes()?;
let final_url = resolved;
let meta_level = nomad_common::ecs::MetadataLevel::Minimal;
let (mut ents, meta_extracted, content_flags, form_entries) =
nomad_core::parse::parse_html_full_with_level(&body, meta_level)?;
let html = String::from_utf8_lossy(&body);
nomad_core::css::parse_and_apply_css(&html, &mut ents);
let (total_w, total_h) = nomad_core::layout::solve(&mut ents, &nomad_core::layout::LayoutConfig::default());
ents.shrink_to_fit();
let frame = nomad_common::ecs::EcsFrame {
tab_id: 0,
viewport: nomad_common::types::Viewport { x: 0., y: 0., width: 800., height: 600. },
scroll_offset: (0., 0.),
total_extent: (total_w, total_h),
entities: ents,
content_flags,
form_entries,
};
let meta = nomad_common::ecs::PageMetadata {
title: meta_extracted.title,
url: final_url,
description: String::new(),
og_title: String::new(),
og_image: String::new(),
og_description: String::new(),
canonical_url: String::new(),
json_ld: Vec::new(),
};
let mode = nomad_core::formatter::OutputMode::from_u8(fmt);
let bytes = nomad_core::formatter::FormatEngine::format(&frame, Some(&meta), mode);
write_output(&bytes, args.output.as_deref())
}
fn format_envelope(data: &serde_json::Value, fmt: u8) -> Vec<u8> {
if fmt == 0 {
if let Some(hex) = data.get("body_hex").and_then(|v| v.as_str()) {
return hex::decode(hex).unwrap_or_default();
}
}
if let Some(raw) = data.get("raw").and_then(|v| v.as_str()) {
return raw.as_bytes().to_vec();
}
if let Some(d) = data.get("data") {
return serde_json::to_string_pretty(d).unwrap_or_default().into_bytes();
}
serde_json::to_string_pretty(data).unwrap_or_default().into_bytes()
}
fn write_output(data: &[u8], path: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
match path {
Some(p) => Ok(std::fs::write(p, data)?),
None => {
if data.first().copied() == Some(0) || data.iter().any(|b| *b == 0 && *b < 32) {
std::io::stdout().write_all(data)?;
} else {
let s = String::from_utf8_lossy(data);
print!("{s}");
}
Ok(())
}
}
}
fn resolve_format(fmt: &str) -> u8 {
if let Ok(n) = fmt.parse::<u8>() {
return n.clamp(0, 10);
}
match fmt.to_lowercase().as_str() {
"raw" => 0, "json" => 1, "markdown" => 2, "md" => 2,
"agent" => 3, "summary" => 4, "minimal" => 5,
"section" => 6, "a11y" => 7, "tree" => 8, "reader" => 9,
"form" => 10,
_ => 4,
}
}