A minimal, Vim-powered web browser written in Oscan.
OscaWeb is a Single Document Interface (SDI) web browser that prioritizes keyboard-driven navigation inspired by Vimium. It supports HTTP/HTTPS (TLS built into Oscan — SChannel on Windows, BearSSL on Linux), renders images inline (PNG, JPEG, BMP, GIF, SVG), and runs inline JavaScript via an embedded QuickJS-ng engine — all with zero external dependencies beyond the Oscan compiler.
- Key Features
- Screenshots
- Prerequisites
- Quick Start
- Keyboard Shortcuts
- HTML Rendering
- Image Pipeline
- JavaScript Engine
- Mouse Interaction
- Link Hint Mode
- Status Bar
- Architecture
- CSS
- Networking
- Testing
- Reader Mode, Bookmarks & Zoom
- Limitations
- Manual Test Plan
- Design Philosophy
- Contributing
- Built With
- License
Ordered from coolest-first. Deeper detail under the linked sections.
- Vim-style keyboard navigation — scroll, follow links, search, and navigate without ever reaching for the mouse. Keyboard Shortcuts.
- Link hint mode — press
fand every link gets a 1–2 letter label; type the label to jump.Ftargets chrome-only links. Link Hint Mode. - Reader mode + chrome trimming + article column —
<main>/<article>auto-narrows to a readable column; sidebars, hamburger menus, and footers are hidden by default.grtoggles reader mode,gRshows the full page. Reader Mode, Bookmarks & Zoom. - Omnibox with DuckDuckGo + history autocomplete — the address bar doubles as a search box; anything non-URL goes to DuckDuckGo. History substring matches auto-suggest as you type. Omnibox & autocomplete.
- Embedded JavaScript (QuickJS-ng) — inline
<script>, external<script src>,onclick, plus a real DOM API (querySelector,classList, …). JavaScript Engine. - HTTP + HTTPS with zero external deps — TLS is built into Oscan (SChannel on Windows, BearSSL on Linux). Chunked transfer-encoding and redirects included. Networking.
- Inline images — PNG, JPEG, BMP, GIF, and SVG decoded, cached, and drawn inline. Image Pipeline.
- CSS with a real cascade — inline
<style>, external<link rel="stylesheet">, and inlinestyle=""parsed with descendant/child combinators, attribute selectors, specificity, and inheritance. CSS. - Rich HTML rendering — 30+ tags including tables (auto-sized columns), lists, blockquotes, and code blocks. HTML Rendering.
- Form submission (GET + POST) —
gffills<form>fields sequentially (text, textarea, checkbox, radio, select) and submits with URL-encoded bodies. Browsing keys. - Persistent HTTP cache — 20-slot FIFO cache survives restarts; back/forward and repeat visits are instant.
rto force a refetch. Networking. - Cookie jar — RFC 6265 subset with disk persistence.
gCclears everything. Networking. - Persistent history & bookmarks — 500 most-recent URLs on disk;
bto bookmark,Bfor the bookmarks panel. History & bookmarks. - Outline / table of contents —
toverlays H1–H3 headings;1–9jumps. Outline / table of contents (t). - In-page search —
/to search,n/Nto navigate matches. - Text selection & copy — click-and-drag to select; released selections hit the clipboard automatically.
- Fragment navigation —
#anchorscrolls to the element; same-page fragments skip the network. - Runtime zoom —
+/-/0between 1× and 4×. - Dark theme — purpose-built color scheme for comfortable reading.
- Minimal dependencies — only the Oscan compiler is required to build.
The default terminal-inspired theme uses an 8×8 bitmap font for that unmistakable DOS/VT100 feel.
Switch to a TrueType font (TT) and bump the size (16pt) for a clean, modern reading experience while keeping every Vim keybinding.
- Oscan compiler
v0.0.43or newer — required (includes TLS support and the collection intrinsics used by OscaWeb). - PowerShell — for the build script
build.ps1 accepts auto, llvm, cranelift, and c. It reads oscan --version metadata and individual help capabilities instead of assuming a backend from the host OS.
| Backend | Runtime | Use |
|---|---|---|
llvm |
hosted libc (--libc) |
Recommended Windows/Linux backend and the release default. |
cranelift |
hosted libc (--libc) |
Alternate object backend for comparison and debugging. |
c |
C backend | Compatibility baseline and the supported macOS backend. |
auto |
capability-dependent | Chooses hosted LLVM when available, otherwise C, then hosted Cranelift. |
OscaWeb compiles QuickJS, miniz, and two bridge files through --extra-c. The stock v0.0.43 LLVM and Cranelift release packages are intentionally toolchain-free and therefore cannot build the full browser. Use the v0.0.43 C package, or build Oscan from a recursive source checkout so toolchain-free: no and --libc, --extra-c, and --extra-cflags are available. The v0.0.43 Linux C archive omits libbearssl.a; place the matching tag's packaging/prebuilt/linux-x86_64/libbearssl.a under the extracted package's toolchain/lib/ directory before building TLS-enabled programs. CI stages that archive automatically. Hosted LLVM CI source-builds Oscan and combines it with the packaged LLVM provider plus the matching packaged C toolchain; Linux pins both runtime-archive compilation and final linking to that package's musl GCC/binutils so static release binaries remain ABI-compatible and portable.
# Choose from installed compiler metadata
.\build.ps1
# Explicit backends
.\build.ps1 -Backend llvm -NativeTarget host
.\build.ps1 -Backend cranelift -NativeTarget host
.\build.ps1 -Backend c
# Object-backend release build: optimize bundled C inputs/final link and strip
.\build.ps1 -Backend llvm -OptimizeSize-Backend native remains a warning-producing compatibility alias for cranelift; it never selects LLVM.
Trusted Windows CI/release jobs pass -AllowElevatedNativeLink, which forwards Oscan's explicit --allow-elevated-native-link opt-in. -OptimizeSize is available for LLVM and Cranelift on Windows/Linux; it optimizes bundled C inputs and the final compiler-driver link. Linux object builds retain -D_GNU_SOURCE -static. The Release workflow's dry_run dispatch input builds and tests every release target without publishing.
# Build and run
.\build.ps1 -Run
# Navigate to a URL on startup
.\build\browser.exe http://example.comPress o once the browser is running to open a URL, or pass one on the command line.
OscaWeb uses Vimium-inspired keybindings. Press ? in the browser to toggle the help overlay.
| Key | Action |
|---|---|
j |
Scroll down |
k |
Scroll up |
d |
Half page down |
u |
Half page up |
Space |
Full page down |
gg |
Scroll to top |
G |
Scroll to bottom |
| Key | Action |
|---|---|
f |
Follow link (hint mode, all links) |
F |
Follow link — chrome links only (nav/header/footer/aside) |
o |
Open URL / search (clear bar) |
O |
Edit current URL |
r |
Reload page |
gr |
Toggle reader mode |
gR |
Toggle show-full (disable chrome trim + user stylesheet, reloads) |
gC |
Clear all cookies (jar + disk file) |
gf |
Fill form on the current page (sequential prompts, Enter advances, Esc aborts) |
gm |
Jump to <main>/<article> / first heading |
t |
Toggle outline (table of contents) |
b |
Bookmark / un-bookmark current URL |
B |
Toggle bookmarks panel |
1–9 |
Jump to Nth item (context: outline headings, or bookmarks when panel is open) |
p |
Paste URL from clipboard and go |
yy |
Copy current URL to clipboard |
H |
Go back in history |
L |
Go forward in history |
| Key | Action |
|---|---|
+ / = |
Zoom in |
- |
Zoom out |
0 |
Reset zoom |
| Key | Action |
|---|---|
/ |
Search in page |
n |
Next match |
N |
Previous match |
| Key | Action |
|---|---|
Esc |
Return to normal mode |
? |
Toggle help overlay |
Q |
Quit browser |
Ctrl+C |
Copy current URL |
Ctrl+V |
Paste in address bar |
| Key | Action |
|---|---|
Ctrl+A |
Move cursor to start |
Ctrl+E |
Move cursor to end |
← → |
Move cursor |
Down / Tab |
Cycle autocomplete suggestion ↓ |
Up |
Cycle autocomplete suggestion ↑ |
Enter |
Navigate to URL (or DuckDuckGo search) |
Esc |
Cancel editing |
The address bar is also an omnibox: anything that does not look like a URL
(scheme, dot in the host, localhost, or host:port) is sent to
DuckDuckGo as a search query. Substring matches against your saved
browsing history appear as autocomplete suggestions while you type.
OscaWeb renders 30+ HTML tags with a dark-themed color scheme:
Text styling — <b>/<strong>, <em>/<i>/<cite>, <del>/<s> (strikethrough), <u>/<ins> (underline), <mark> (highlight), <code>, <pre>
Structure — <h1>–<h6>, <p>, <div>, <blockquote> (indented with accent bar), <hr>, <br>, <section>, <article>, <nav>, <header>, <footer>, <main>, <figure>/<figcaption>
Lists — <ul> (bullets), <ol> (numbered), <li>, <dl>/<dt>/<dd> (definition lists)
Tables — <table>, <thead>/<tbody>/<tfoot>, <tr>, <td>/<th> with automatic column-width calculation, header separators, and cell truncation
Links & images — <a> (clickable, underlined, hint-followable), <img> (fetched, decoded, cached, and scaled inline)
Entities — &, <, >, —, –, …, ©, ®, ™, •, ←, →, and more
Images are fetched via HTTP/HTTPS, decoded, and rendered inline:
- Formats — PNG, JPEG, BMP, GIF (raster via
img_load()), SVG (rasterized viasvg_load()) - Caching — decoded pixel data cached per page; cleared on navigation
- Scaling — images wider than 1000px are downscaled via nearest-neighbor at cache time
- HTML attributes —
width/heightsupported (absolute pixels and percentages) - SVG compositing — rendered over light gray background for icon visibility
- Fallback —
[IMG: alt text]placeholder if decoding fails
OscaWeb embeds QuickJS-ng for JavaScript execution via a C bridge (js_bridge.c):
- Inline scripts —
<script>...</script>blocks executed after page load - onclick handlers — elements with
onclickattributes are clickable; code evaluated on click - DOM dirty tracking — JS modifications to the DOM trigger automatic re-render
// Document methods
document.getElementById("myId") // → Element or null
document.getElementsByTagName("div") // → Element[]
document.getElementsByClassName("btn") // → Element[]
document.querySelector("#main .title") // → Element or null
document.querySelectorAll("a.ext") // → Element[]
// Element properties
element.tagName // getter
element.textContent // getter/setter
element.children // getter → child Element[]
element.id // getter
element.className // getter/setter (class attribute)
element.classList // getter → DOMTokenList { add, remove, toggle, contains }
// Element methods
element.getAttribute("href")
element.setAttribute("class", "active")
element.querySelector(".note") // scoped like document.querySelector
element.querySelectorAll("li")
element.addEventListener("click", fn) // accepted, not dispatched
element.removeEventListener("click", fn) // accepted, not dispatched
// Console
console.log("hello")
console.warn("warning")
console.error("error")Selector subset supported by querySelector/querySelectorAll: tag,
.class, #id, *, compound (a.btn, div#main), comma-separated lists,
and the descendant combinator (nav a, #main .title).
External scripts — <script src="..."> is fetched (same origin rules
as <link rel="stylesheet">) and evaluated after inline scripts in
document order.
- Click links — click any link to navigate
- Click onclick elements — triggers JavaScript handler
- Text selection — click and drag to select text; released selection is automatically copied to clipboard
- Address bar — click to focus, click to reposition cursor within URL
Press f to enter Follow mode. Each link gets a hint label:
- Single-letter hints for pages with few links (
a,s,d,f, ...) - Double-letter hints for pages with many links (
aa,as,ad, ...) - Type the hint letters to navigate to the corresponding link
- If no labels match your typed prefix, Follow mode exits automatically
Press F (capital) to label only chrome links (nav/header/footer/aside) — useful for jumping to "edit", "talk", or site-nav items that are hidden by the default chrome trimming.
The bottom bar shows at a glance:
- Mode —
NORMAL,INSERT,FOLLOW, orSEARCH - Scroll position —
Topor percentage (e.g.,42%) - Link count — number of links on the current page
- Search results —
[2/5]match counter when searching
OscaWeb is split into focused Oscan modules (browser.osc, html.osc,
css.osc, http.osc, js.osc, …) plus a single C bridge to QuickJS-ng.
For the module graph, per-module descriptions, and the on-disk file
layout, see docs/ARCHITECTURE.md.
OscaWeb ships a small CSS engine (css.osc) that parses <style> blocks,
external <link rel="stylesheet"> resources, and style="" attributes,
matches a simple selector subset against the DOM, runs the CSS 2.1
cascade, and propagates inheritable properties.
- Selectors —
tag,.class,#id,*, compounds (h1.title), comma-separated selector lists, the descendant combinator (nav a,article .title), the child combinator (div > p), and attribute selectors ([attr],[attr=value],[attr~=tok],[attr^=prefix],[attr$=suffix],[attr*=substr]) - Properties —
color,background-color/background,font-weight,font-style,text-decoration(underline/line-through/none),text-align(left/center/right),display: none,padding(shorthand + T/R/B/L),widthandmax-width(px or %),line-height(unitless multiplier, px, ornormal), andmargin: 0 auto(sentinel for block centering; full margin box is not otherwise modelled) - Values — named colors (subset),
#rgb,#rrggbb,rgb(r, g, b),bold/normal(and numeric weights),italic,!important - Cascade — specificity + source order across inline
<style>and external<link rel="stylesheet">blocks in document order; inlinestyle=""wins over stylesheet rules;!importantwins over non-!important - Inheritance —
color,font-weight,font-style,text-decoration,text-align,line-heightpropagate from parent to child @media—prefers-color-scheme: darkapplies (OscaWeb is always-dark);prefers-color-scheme: lightis dropped; all other at-rule preludes (print,min-width, …) are dropped
Because OscaWeb is a terminal-style renderer with a monospace bitmap
font, font-weight: bold and font-style: italic are approximated by
switching to the bold/italic accent color rather than changing glyph
shape. text-align is applied by measuring the inline text width of a
block and prepositioning the cursor before children render; it only
fires when the content fits on a single line (multi-line wrapping falls
back to left alignment).
- Sibling combinators (
+/~) — rules containing them are parsed and skipped - Pseudo-classes / pseudo-elements (
:hover,::before, …) - Box-model properties beyond
padding/width/max-width/numericmargin(noheight,border,float,position,flex,grid) - Units other than unitless integers for
rgb()and bare hex colors @mediabeyondprefers-color-scheme(other at-rules are parsed and ignored;@importand@font-faceare not expanded)
# Serve the bundled smoke-test page, then open it in the browser
cd tests
python -m http.server 8000
# in another shell:
.\build\browser.exe http://localhost:8000/test_page_css.htmlSee tests/test_page_css.html for a compact page that exercises every
supported CSS feature.
- HTTP/1.1 with chunked
Transfer-Encodingdecoding, automaticUser-Agent: OscaWeb/0.1header, andConnection: close(we still do one request per socket, but servers that don't send aContent-Lengthand instead stream chunked responses now work) - TLS built-in — SChannel on Windows, BearSSL on Linux (zero external dependencies)
- Redirects — automatic follow of 301/302/303/307/308 (up to 5 hops)
- Default ports — 80 for HTTP, 443 for HTTPS
- Persistent HTTP cache — text responses (HTML, CSS, JS, JSON, XML)
survive browser restarts; saved to
%APPDATA%/oscanweb_cache.txt(or$HOME/...on POSIX) on exit and reloaded at startup. Binary resources (images) are cached in-memory only. Pressrto invalidate the current URL and force a refetch.
- Scheme detection (
http://,https://, defaulthttps) - Relative URL resolution (
../,./, absolute paths, protocol-relative//) - Query string and fragment (
#anchor) preservation; fragment-only navigation skips the network - Auto-prepends
https://when no scheme is entered (matches modern browsers; many sites, e.g.www.microsoft.com, return 403 on plain HTTP)
# Run all unit tests (offline, CI-safe)
.\build.ps1 -Test
# Run tests through a specific backend
.\build.ps1 -Backend c -Test
.\build.ps1 -Backend llvm -Test
.\build.ps1 -Backend cranelift -Test
# Run individual test suites
oscan tests/test_url.osc --run
oscan tests/test_html.osc --runOn Linux, test_js.osc is skipped only for the C/freestanding path because QuickJS is known to crash there. Hosted LLVM and Cranelift builds run the JS coverage.
tests/test_pages.osc parses captured HTML fixtures from sites we've
previously hit rendering bugs on (HN, danluu.com, BMFW, RFC datatracker,
W3C CSS2 spec, Wikipedia) and asserts page-specific invariants — each
one corresponds to a fix we shipped. Run as part of build.ps1 -Test.
Refresh the captured snapshots when sites change layout:
.\tools\capture_fixtures.ps1.\tools\smoke.ps1Hits the public internet to verify HTTPS/TLS, SNI hostname handling,
redirects, Host header construction, gzip/chunked/content-length body
handling, and HTML parsing end-to-end, including the lucabol.com regression
URL. Flaky by design; not part of CI. A self-signed local TLS fixture would
require disabling certificate validation or changing host trust stores, so
offline tests keep coverage to deterministic request construction and
response parsing. Use after touching http.osc, gzip_bridge.c, or
TLS-related code.
When a page contains a <main> or <article> element, OscaWeb narrows
that subtree to a readable ~640 CSS px and centers it (scales with the
current zoom level), so article pages don't run edge-to-edge on wide
canvases. Pages that already set their own max-width on the main
content are left alone, as are utility pages with no <main> or
<article> (homepages, search results, dashboards) — those keep the
full viewport width.
On heavy pages like Wikipedia, the hamburger menus, sidebars, language lists, and footer chrome can dominate the viewport. By default OscaWeb:
- Uses
<main>/<article>as the render root when present, cutting site chrome at the tree level. - Applies a built-in user stylesheet that hides common chrome
selectors (
nav,aside,.sidebar,.hamburger,#mw-panel,.vector-main-menu,.navbox, etc.) with!important. - Skips
<form>,aria-hidden="true", andhiddenattribute nodes while rendering.
Press gR to toggle show-full — this disables all three trimming
layers and reloads the page so you can see the original document.
The status bar shows FULL when this mode is active. Press F (capital)
for follow-mode hints on chrome-only links (useful to jump to
"edit", "talk", or site-nav items that were hidden).
Press t to toggle an outline panel listing H1/H2/H3 headings in
document order. 1–9 jumps to the Nth heading. gm jumps straight
to the first heading (handy on pages that have no <main>).
Toggles a distraction-free view on top of the article-column default.
If the page contains a <main> or <article> element, the renderer
uses that subtree as the document root. Otherwise, <nav>, <header>,
<footer>, <aside>, and <form> elements are hidden. The same
~640 CSS px column applies. Press gr again to return to the normal
view.
Links to #anchor targets scroll the matching element to the top of the
viewport. When the only difference between the link target and the
current URL is the fragment, OscaWeb skips the network request entirely
and scrolls in-place.
The address bar doubles as a search box. Press o and type:
- URL-like input (contains
://, a dot in the host part,localhost, orhost:port) → navigated directly - Anything else → sent to DuckDuckGo as
https://duckduckgo.com/?q=...
As you type, substring matches from your saved browsing history appear
below the address bar. Down/Tab and Up cycle through the
suggestions; Enter opens the highlighted one (or the literal text if
nothing is selected).
- History is stored one URL per line in
%APPDATA%\oscaweb_history.txt(or$HOME/oscaweb_history.txton Linux), most-recent first, capped at 500 entries. - Bookmarks live in
oscaweb_bookmarks.txtnext to the history file. Pressbon any page to toggle it;Bopens a panel listing all bookmarks; pressing1–9while the panel is open jumps to the matching entry.
+ / = zooms in, - zooms out, 0 resets. The current zoom factor
is shown in the status bar. Zoom is clamped between 1× and 4× and scales
headings, paragraphs, code blocks, and tables uniformly.
- No full CSS layout — colors, weights, decorations, backgrounds,
display:none,padding,width/max-widthand numericmarginare honored, but there is noheight,border, flexbox/grid, orfloat/position - CSS selectors —
tag,.class,#id,*, comma-separated lists, descendant combinator (), child combinator (>), and attribute selectors ([attr],[attr=v],[attr~=v],[attr^=v],[attr$=v],[attr*=v]). Sibling combinators (+,~) and pseudo-classes (:hover,::before) are not matched - Limited form submission — GET and POST forms supported via
gfwith text, textarea, checkbox, radio, and select fields (no visual inline rendering of the fields; prompting is status-bar-driven) - HTTP Content-Encoding: gzip / deflate are decoded transparently via miniz (RFC 1952 gzip and RFC 1950 zlib). Brotli and other encodings are not supported and produce a clear error rather than a hang.
- Fixed viewport — 1024×768 window with 8×8 monospace bitmap font
- Single-threaded — synchronous page fetching
End-to-end smoke-test recipes live in docs/MANUAL_TESTS.md.
They cover chunked transfer-encoding, external <script src>,
checkbox/radio/select form flows, CSS attribute + child selectors, JS DOM
APIs, and the persistent HTTP cache.
- SDI (Single Document Interface) — one window, no tabs, maximum simplicity
- Keyboard-first — Vim-inspired navigation means your hands never leave the home row
- Oscan showcase — demonstrates the language's ability to build real applications with C interop
- Zero external dependencies — only the Oscan compiler is needed; TLS, image decoding, and JS are all built in
-
Build & test before sending a change:
.\build.ps1 -Test -
Match the existing Oscan style — see
.github/copilot-instructions.mdif present, or the conventions documented throughout this README (no allocating mutation of persistent arrays insidearena { }, usearray_clearfor in-place clearing, etc.). -
Keep commits focused and include a short description of what and why.
- Oscan — Minimalist language designed for LLM code generation, with LLVM, Cranelift, and C backends
- QuickJS-ng — Lightweight JavaScript engine (embedded via C bridge)
- Vimium — Inspiration for the keyboard shortcut scheme
This project is licensed under the MIT License.

