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 let fmt_id = resolve_format(&args.format);
9
10 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 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 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 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 let html = String::from_utf8_lossy(&body);
53 nomad_core::css::parse_and_apply_css(&html, &mut ents);
54
55 let (total_w, total_h) = nomad_core::layout::solve(&mut ents, &nomad_core::layout::LayoutConfig::default());
57 ents.shrink_to_fit();
58
59 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 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 std::io::stdout().write_all(data)?;
108 } else {
109 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}