Skip to content

Step 3: ie-shell — Headless mode and CLI #6

@thomasnemer

Description

@thomasnemer

Parent: #2

Goal

Add CLI argument parsing and headless mode so the browser can run without a window. This is the foundation for e2e testing — headless mode must work from day one. Headless path skips winit entirely (no event loop, no window).

Current State

crates/ie-shell/src/main.rs has a Browser struct implementing winit ApplicationHandler. It creates a maximized window and handles CloseRequested/RedrawRequested. No CLI args, no headless mode.

crates/ie-shell/Cargo.toml depends on all 9 other workspace crates (including Phase 2 crates).

File Changes

  • src/main.rs — restructure: parse CLI, branch on mode
  • src/cli.rs — new file, CLI argument definitions
  • src/app.rs — new file, extract Browser struct from main.rs
  • src/headless.rs — new file, headless execution path
  • Cargo.toml — add clap, remove Phase 2 deps

Workspace-level prerequisite

Add to root Cargo.toml [workspace.dependencies]:

  • clap = { version = "4", features = ["derive"] }

Dependency cleanup in ie-shell/Cargo.toml

Remove Phase 2 crate dependencies to speed up compilation:

  • Remove ie-html
  • Remove ie-css
  • Remove ie-js
  • Remove ie-layout
  • Remove ie-render
  • Remove ie-wasm

Keep: ie-net, ie-dom, ie-sandbox, anyhow, thiserror, tracing, tracing-subscriber, tokio, winit, url
Add: clap

Implementation

CLI argument parsing (cli.rs)

  • Cli struct with clap derive:
    #[derive(Parser)]
    #[command(name = "internet-exploder", about = "A minimal, private-by-default web browser")]
    pub struct Cli {
        /// Run without a window
        #[arg(long)]
        pub headless: bool,
    
        /// URL to navigate to on startup
        #[arg(long)]
        pub url: Option<String>,
    
        /// Print page source to stdout and exit (requires --headless and --url)
        #[arg(long, requires_all = ["headless", "url"], conflicts_with = "dump_status")]
        pub dump_source: bool,
    
        /// Print HTTP status code to stdout and exit (requires --headless and --url)
        #[arg(long, requires_all = ["headless", "url"], conflicts_with = "dump_source")]
        pub dump_status: bool,
    
        /// Allow plain HTTP navigation (default: HTTPS-only)
        #[arg(long)]
        pub allow_http: bool,
    }
  • Mode enum:
    pub enum Mode {
        Gui { url: Option<Url> },
        Headless { url: Option<Url>, action: HeadlessAction },
    }
    
    pub enum HeadlessAction {
        DumpSource,
        DumpStatus,
        Interactive,
    }
  • Cli::mode(&self) -> Result<Mode>:
    • If headless:
      • Parse url if present → InvalidUrl on failure
      • Determine action: dump_source → DumpSource, dump_status → DumpStatus, neither → Interactive
      • Return Mode::Headless { url, action }
    • Else:
      • Parse url if present
      • Return Mode::Gui { url }

Main restructuring (main.rs)

  • fn main() -> Result<()>:
    let cli = Cli::parse();
    init_tracing();
    match cli.mode()? {
        Mode::Gui { url } => run_gui(url),
        Mode::Headless { url, action } => run_headless(url, action),
    }
  • fn init_tracing() — existing tracing_subscriber setup
  • fn run_gui(url: Option<Url>) -> Result<()> — existing winit event loop code, moved from current main

Browser app struct (app.rs)

  • Move Browser struct and its ApplicationHandler impl here
  • Browser::new(url: Option<Url>) -> Self — accepts optional startup URL
  • Keep existing behavior: create window on resumed, handle close/redraw

Headless execution path (headless.rs)

  • pub fn run_headless(url: Option<Url>, action: HeadlessAction) -> Result<()>:
    • Create tokio runtime
    • runtime.block_on(async { ... })
    • Match on action:
      • DumpSource → placeholder: println!("source dump not yet wired (need ie-net)") + exit 0
      • DumpStatus → placeholder: println!("status dump not yet wired (need ie-net)") + exit 0
      • Interactive → placeholder: eprintln!("interactive headless mode not yet implemented") + exit 0
    • Note: actual implementation comes in Step 6 after ie-net is wired in. This step just establishes the code structure and branching.

Tests

CLI parsing unit tests (in cli.rs)

Use Cli::try_parse_from(["ie", ...]) for testing:

  • No args → Mode::Gui { url: None }
  • --url https://example.comMode::Gui { url: Some(https://example.com) }
  • --headlessMode::Headless { url: None, action: Interactive }
  • --headless --url https://example.comMode::Headless { url: Some(...), action: Interactive }
  • --headless --dump-source --url https://example.comMode::Headless { action: DumpSource }
  • --headless --dump-status --url https://example.comMode::Headless { action: DumpStatus }
  • --dump-source without --headless → parse error
  • --dump-source without --url → parse error
  • --dump-source --dump-status → parse error (conflicts)
  • --url not-a-urlMode::Gui with URL parsing (may succeed as relative URL — decide behavior)
  • --allow-http flag is accepted and stored

Smoke test (integration, in crates/ie-shell/tests/)

  • Spawn binary with --headless --dump-status --url https://example.com, verify it exits with code 0 (placeholder output is fine)
  • Spawn binary with --headless, verify it exits with code 0

Acceptance Criteria

  • cargo run -p ie-shell — window opens (existing behavior preserved)
  • cargo run -p ie-shell -- --headless — runs and exits without opening a window
  • cargo run -p ie-shell -- --headless --dump-status --url https://example.com — exits cleanly with placeholder output
  • cargo test -p ie-shell — CLI parsing tests pass
  • cargo clippy -p ie-shell -- -D warnings — no warnings
  • cargo fmt -p ie-shell --check — formatted
  • Build time for ie-shell is noticeably faster after removing Phase 2 deps

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions