mod ai; mod app; mod browser; mod chat; mod config; mod mascot; mod memory; mod rag; mod scrape; mod search; mod storage; mod tabs; mod ui; use anyhow::{Context, Result}; use app::App; use browser::{Browser, RenderMode}; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{backend::CrosstermBackend, Terminal}; use std::env; use std::io; use std::time::{Duration, Instant}; use search::{QueryTarget, SearchManager}; const VERSION: &str = "3.0.0"; fn print_help() { println!( r#"Azul-Browse v{} - Terminal Web Browser USAGE: azul-browse [OPTIONS] azul-browse -q azul-browse --js -q OPTIONS: -q, --query Search or fetch URL in CLI mode --js Enable JavaScript rendering (headless Chrome) --test-http Run HTTP client tests -h, --help Show this help message -v, --version Show version SEARCH PREFIXES: w: Wikipedia search a: arXiv search (academic papers) s: Google Scholar search d: DuckDuckGo search g: General web search (DuckDuckGo) p: PubMed search (medical) ol: OpenLibrary search (books) Without prefix, searches DuckDuckGo by default. Direct URLs (containing '.' without spaces) go directly to the site. TUI KEYBINDINGS: / Focus URL bar Tab Cycle focus (Content -> Sidebar -> URL Bar) j/k, ↓/↑ Scroll content or navigate links g/G Go to top/bottom of content Enter Open selected link (in sidebar) 1, 2 Focus Content (1) or Sidebar (2) ? Toggle help q, Ctrl+C Quit EXAMPLES: azul-browse # Start TUI browser azul-browse -q "rust programming" # Search DuckDuckGo azul-browse -q "w:Rust language" # Search Wikipedia azul-browse -q "a:neural networks" # Search arXiv azul-browse -q "https://example.com" # Fetch URL directly azul-browse --test-http # Test HTTP client ENVIRONMENT VARIABLES: OPENROUTER_API_KEY API key for AI summaries (optional) "#, VERSION ); } fn print_version() { println!("azul-browse {}", VERSION); } fn main() -> Result<()> { let args: Vec = env::args().collect(); // Check for --js flag let js_mode = args.iter().any(|a| a == "--js"); // Parse arguments for (i, arg) in args.iter().enumerate() { match arg.as_str() { "-h" | "--help" => { print_help(); return Ok(()); } "-v" | "--version" => { print_version(); return Ok(()); } "-q" | "--query" => { if let Some(query) = args.get(i + 1) { if query == "--js" { // --js came after -q, get next arg if let Some(q) = args.get(i + 2) { return run_cli_mode(q, js_mode); } } return run_cli_mode(query, js_mode); } else { eprintln!("Error: -q requires a query argument"); return Ok(()); } } "--test-http" => { return test_http_client(); } _ => {} } } // Legacy support: -q query (where query is multiple args) if args.len() >= 3 && args[1] == "-q" { let query_parts: Vec<&String> = args[2..].iter().filter(|a| *a != "--js").collect(); let query = query_parts.iter().map(|s| s.as_str()).collect::>().join(" "); return run_cli_mode(&query, js_mode); } // Start TUI mode run_tui() } fn run_tui() -> Result<()> { // Setup panic hook to restore terminal on crash let original_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |panic_info| { // Restore terminal let _ = disable_raw_mode(); let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture); original_hook(panic_info); })); // Setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; // Create app let mut app = App::new()?; let tick_rate = Duration::from_millis(app.config.refresh_rate_ms); // Run app let res = run_app(&mut terminal, &mut app, tick_rate); // Restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { eprintln!("Error: {:?}", err); } Ok(()) } fn run_app( terminal: &mut Terminal, app: &mut App, tick_rate: Duration, ) -> Result<()> { let mut last_tick = Instant::now(); loop { terminal.draw(|f| ui::render(f, app))?; let timeout = tick_rate.saturating_sub(last_tick.elapsed()); if event::poll(timeout)? { if let Event::Key(key) = event::read()? { // Handle quit specially if matches!(key.code, KeyCode::Char('c')) && key.modifiers.contains(event::KeyModifiers::CONTROL) { app.quit(); } else { app.handle_key(key)?; } } } if last_tick.elapsed() >= tick_rate { app.update(); last_tick = Instant::now(); } if app.should_quit { break; } } Ok(()) } /// CLI mode - fetch a URL or search query and display results fn run_cli_mode(query: &str, js_mode: bool) -> Result<()> { println!("Azul CLI Mode v{}", VERSION); println!("Query: {}", query); if js_mode { println!("Mode: JavaScript rendering (headless Chrome)"); } println!("---"); let render_mode = if js_mode { RenderMode::JavaScript } else { RenderMode::Static }; match search::classify_query(query) { QueryTarget::Url(url) => { println!("Fetching{}: {}", if js_mode { " [JS]" } else { "" }, url); println!(); let browser = Browser::new().context("Failed to create browser")?; let page = browser.fetch_with_mode(&url, render_mode).context("Failed to fetch page")?; print_page(&page); } QueryTarget::Search { engine, query } => { println!("Searching {} for: {}", engine.name(), query); println!(); let response = SearchManager::new() .and_then(|manager| manager.search_with(engine, &query)) .context("Search failed")?; let page = search::results_to_page(response); print_page(&page); } QueryTarget::MultiSearch { query } => { println!("Multi-engine search for: {}", query); println!(); let manager = SearchManager::new().context("Search manager init failed")?; let response = manager.search_aggregated(&query); let page = response.to_page(); print_page(&page); } } Ok(()) } fn print_page(page: &browser::Page) { println!("Title: {}", page.title); println!("URL: {}", page.url); println!("Links found: {}", page.links.len()); println!(); println!("--- Content Preview (first 50 lines) ---"); for (i, line) in page.content_lines.iter().take(50).enumerate() { println!("{:4}: {}", i + 1, line); } if page.content_lines.len() > 50 { println!("... ({} more lines)", page.content_lines.len() - 50); } println!(); println!("--- Links ---"); for (i, link) in page.links.iter().take(20).enumerate() { println!("{:3}. {} -> {}", i + 1, link.text, link.url); } if page.links.len() > 20 { println!("... ({} more links)", page.links.len() - 20); } } /// Test HTTP client creation and basic fetch fn test_http_client() -> Result<()> { println!("Testing HTTP client..."); println!(); // Test 1: Create client with minimal options println!("Test 1: Creating minimal reqwest client..."); let _client = reqwest::blocking::Client::builder() .build() .context("Failed to create minimal client")?; println!(" OK: Minimal client created"); // Test 2: Create client with timeout println!("Test 2: Creating client with timeout..."); let _client = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(30)) .build() .context("Failed to create client with timeout")?; println!(" OK: Client with timeout created"); // Test 3: Create client with user agent println!("Test 3: Creating client with user agent..."); let client = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(30)) .user_agent("Azul-Browse/3.0 (Terminal Browser)") .build() .context("Failed to create client with user agent")?; println!(" OK: Client with user agent created"); // Test 4: Fetch example.com println!("Test 4: Fetching example.com..."); let response = client .get("https://example.com") .send() .context("Failed to send request")?; println!(" OK: Got response with status {}", response.status()); // Test 5: Read response body println!("Test 5: Reading response body..."); let body = response.text().context("Failed to read body")?; println!(" OK: Body length = {} bytes", body.len()); // Test 6: Test Browser struct println!("Test 6: Creating Browser struct..."); let browser = Browser::new().context("Failed to create Browser")?; println!(" OK: Browser created"); // Test 7: Fetch via Browser println!("Test 7: Fetching example.com via Browser..."); let page = browser .fetch("https://example.com") .context("Failed to fetch via Browser")?; println!(" OK: Page fetched - title: {}", page.title); println!(" Content lines: {}", page.content_lines.len()); println!(" Links found: {}", page.links.len()); println!(); println!("All HTTP tests passed!"); Ok(()) }