Skip to main content

nomad_cli/
browse.rs

1use std::io::Write;
2
3use crate::cli::BrowseArgs;
4use crate::client::{self, Envelope};
5
6pub fn run(args: BrowseArgs) -> Result<(), Box<dyn std::error::Error>> {
7    // Resolve format string to numeric ID
8    let fmt_id = resolve_format(&args.format);
9
10    // Check if daemon is available
11    if !args.oneshot && client::daemon_alive(args.port) {
12        browse_via_daemon(&args, fmt_id)
13    } else {
14        browse_oneshot(&args, fmt_id)
15    }
16}
17
18fn browse_via_daemon(args: &BrowseArgs, fmt: u8) -> Result<(), Box<dyn std::error::Error>> {
19    let url = format!("http://127.0.0.1:{}/api/navigate", args.port);
20    let body = serde_json::json!({"url": args.url, "format": fmt});
21    let client = reqwest::blocking::Client::new();
22    let resp = client.post(&url).json(&body).send()?;
23    let env: Envelope = resp.json()?;
24
25    if !env.success {
26        let err = env.error.unwrap_or_else(|| "request failed".into());
27        eprintln!("API error: {err}");
28        // Fall back to oneshot
29        return browse_oneshot(args, fmt);
30    }
31
32    let output = format_envelope(&env.data, fmt);
33    write_output(&output, args.output.as_deref())
34}
35
36fn browse_oneshot(args: &BrowseArgs, fmt: u8) -> Result<(), Box<dyn std::error::Error>> {
37    let resolved = nomad_core::omnibox::resolve(&args.url);
38
39    // Fetch via blocking HTTP client
40    let client = nomad_core::net::build_blocking_client()
41        .ok_or_else(|| "failed to init HTTP client (no TLS?)")?;
42    let resp = client.get(&resolved).send()?;
43    let body = resp.bytes()?;
44    let final_url = resolved;
45
46    // Parse
47    let meta_level = nomad_common::ecs::MetadataLevel::Minimal;
48    let (mut ents, meta_extracted, content_flags, form_entries) =
49        nomad_core::parse::parse_html_full_with_level(&body, meta_level)?;
50
51    // CSS
52    let html = String::from_utf8_lossy(&body);
53    nomad_core::css::parse_and_apply_css(&html, &mut ents);
54
55    // Layout
56    let (total_w, total_h) = nomad_core::layout::solve(&mut ents, &nomad_core::layout::LayoutConfig::default());
57    ents.shrink_to_fit();
58
59    // Build frame
60    let frame = nomad_common::ecs::EcsFrame {
61        tab_id: 0,
62        viewport: nomad_common::types::Viewport { x: 0., y: 0., width: 800., height: 600. },
63        scroll_offset: (0., 0.),
64        total_extent: (total_w, total_h),
65        entities: ents,
66        content_flags,
67        form_entries,
68    };
69    let meta = nomad_common::ecs::PageMetadata {
70        title: meta_extracted.title,
71        url: final_url,
72        description: String::new(),
73        og_title: String::new(),
74        og_image: String::new(),
75        og_description: String::new(),
76        canonical_url: String::new(),
77        json_ld: Vec::new(),
78    };
79
80    let mode = nomad_core::formatter::OutputMode::from_u8(fmt);
81    let bytes = nomad_core::formatter::FormatEngine::format(&frame, Some(&meta), mode);
82    write_output(&bytes, args.output.as_deref())
83}
84
85fn format_envelope(data: &serde_json::Value, fmt: u8) -> Vec<u8> {
86    if fmt == 0 {
87        // RawBincode: hex-encoded in JSON response
88        if let Some(hex) = data.get("body_hex").and_then(|v| v.as_str()) {
89            return hex::decode(hex).unwrap_or_default();
90        }
91    }
92    if let Some(raw) = data.get("raw").and_then(|v| v.as_str()) {
93        return raw.as_bytes().to_vec();
94    }
95    if let Some(d) = data.get("data") {
96        return serde_json::to_string_pretty(d).unwrap_or_default().into_bytes();
97    }
98    serde_json::to_string_pretty(data).unwrap_or_default().into_bytes()
99}
100
101fn write_output(data: &[u8], path: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
102    match path {
103        Some(p) => Ok(std::fs::write(p, data)?),
104        None => {
105            if data.first().copied() == Some(0) || data.iter().any(|b| *b == 0 && *b < 32) {
106                // Binary format — write to stdout as bytes
107                std::io::stdout().write_all(data)?;
108            } else {
109                // Text format — try to print as UTF-8
110                let s = String::from_utf8_lossy(data);
111                print!("{s}");
112            }
113            Ok(())
114        }
115    }
116}
117
118fn resolve_format(fmt: &str) -> u8 {
119    if let Ok(n) = fmt.parse::<u8>() {
120        return n.clamp(0, 10);
121    }
122    match fmt.to_lowercase().as_str() {
123        "raw" => 0, "json" => 1, "markdown" => 2, "md" => 2,
124        "agent" => 3, "summary" => 4, "minimal" => 5,
125        "section" => 6, "a11y" => 7, "tree" => 8, "reader" => 9,
126        "form" => 10,
127        _ => 4,
128    }
129}