350 lines · 10.4 KB · Rust
RawDownload
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 <QUERY>    azul-browse --js -q <URL> OPTIONS:    -q, --query <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:<query>    Wikipedia search    a:<query>    arXiv search (academic papers)    s:<query>    Google Scholar search    d:<query>    DuckDuckGo search    g:<query>    General web search (DuckDuckGo)    p:<query>    PubMed search (medical)    ol:<query>   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<String> = 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::<Vec<_>>().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<B: ratatui::backend::Backend>(    terminal: &mut Terminal<B>,    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 resultsfn 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 fetchfn 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(())}