<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ezcal - Ergonomic iCalendar + vCard for Rust</title>
<meta name="description" content="The simplest way to read and write .ics and .vcf files in Rust. iCalendar (RFC 5545) + vCard (RFC 6350) in one crate.">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
a { color: #60a5fa; text-decoration: none; }
a:hover { text-decoration: underline; }
nav {
position: sticky;
top: 0;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid #1e293b;
padding: 12px 20px;
z-index: 100;
}
nav .nav-inner {
max-width: 1100px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
}
nav .nav-brand {
font-size: 1.3rem;
font-weight: 800;
background: linear-gradient(90deg, #60a5fa, #a78bfa, #f472b6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
nav .nav-links {
display: flex;
gap: 25px;
list-style: none;
}
nav .nav-links a {
color: #94a3b8;
font-size: 0.9rem;
font-weight: 500;
}
nav .nav-links a:hover {
color: #e2e8f0;
text-decoration: none;
}
.hero {
text-align: center;
padding: 80px 20px 60px;
background: linear-gradient(180deg, #1e3a5f 0%, #0f172a 100%);
}
.hero h1 {
font-size: 4rem;
margin-bottom: 10px;
background: linear-gradient(90deg, #60a5fa, #a78bfa, #f472b6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero .tagline {
font-size: 1.5rem;
color: #94a3b8;
margin-bottom: 30px;
}
.hero .sub-tagline {
font-size: 1rem;
color: #64748b;
margin-bottom: 30px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.size-badge {
display: inline-flex;
align-items: center;
gap: 15px;
background: #1e293b;
padding: 12px 25px;
border-radius: 50px;
font-family: monospace;
font-size: 1.1rem;
margin-bottom: 40px;
}
.size-badge .label { color: #64748b; }
.size-badge .rfc { color: #a78bfa; font-weight: bold; }
.size-badge .plus { color: #64748b; }
.hero-buttons {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
margin-bottom: 40px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 28px;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover { transform: translateY(-2px); text-decoration: none; }
.btn-primary {
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
color: white;
}
.btn-github {
background: #1e293b;
color: #e2e8f0;
border: 1px solid #334155;
}
.btn-crates {
background: #e6832a;
color: white;
}
.install-box {
display: inline-block;
background: #1e293b;
padding: 15px 30px;
border-radius: 8px;
font-family: monospace;
font-size: 1.1rem;
border: 1px solid #334155;
}
.install-box .cmd { color: #4ade80; }
.install-box .pkg { color: #f8fafc; }
.container {
max-width: 1100px;
margin: 0 auto;
padding: 60px 20px;
}
.section-title {
text-align: center;
font-size: 2rem;
margin-bottom: 40px;
color: #f8fafc;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 60px;
}
.feature {
background: #1e293b;
padding: 25px;
border-radius: 12px;
border: 1px solid #334155;
}
.feature-icon {
font-size: 2rem;
margin-bottom: 10px;
}
.feature h3 {
color: #f8fafc;
margin-bottom: 8px;
}
.feature p {
color: #94a3b8;
font-size: 0.95rem;
}
.examples-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(480px, 1fr));
gap: 25px;
}
@media (max-width: 540px) {
.examples-grid { grid-template-columns: 1fr; }
}
.example-card {
background: #1e293b;
border-radius: 16px;
overflow: hidden;
border: 1px solid #334155;
}
.example-header {
padding: 15px 20px;
border-bottom: 1px solid #334155;
display: flex;
align-items: center;
gap: 10px;
}
.example-header h3 {
color: #f8fafc;
font-size: 1rem;
}
.example-body {
padding: 0;
}
pre {
background: #0f172a;
padding: 20px;
overflow-x: auto;
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
font-size: 0.85rem;
line-height: 1.7;
}
.comment { color: #64748b; }
.keyword { color: #c084fc; }
.func { color: #60a5fa; }
.string { color: #4ade80; }
.type { color: #f472b6; }
.number { color: #fb923c; }
.macro { color: #fbbf24; }
.punct { color: #94a3b8; }
.api-section {
margin-top: 60px;
}
.api-category {
margin-bottom: 40px;
}
.api-category h3 {
color: #a78bfa;
font-size: 1.3rem;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #334155;
}
.api-table {
width: 100%;
border-collapse: collapse;
background: #1e293b;
border-radius: 12px;
overflow: hidden;
}
.api-table th {
background: #0f172a;
padding: 12px 15px;
text-align: left;
font-weight: 600;
color: #94a3b8;
font-size: 0.85rem;
text-transform: uppercase;
}
.api-table td {
padding: 12px 15px;
border-bottom: 1px solid #334155;
}
.api-table tr:last-child td {
border-bottom: none;
}
.api-table code {
background: #0f172a;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.9rem;
color: #60a5fa;
}
.api-table .desc {
color: #94a3b8;
}
.comparison {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 40px;
}
.comparison-item {
background: #1e293b;
padding: 20px;
border-radius: 12px;
text-align: center;
border: 1px solid #334155;
}
.comparison-item.highlight {
border-color: #4ade80;
background: linear-gradient(180deg, #1e293b 0%, #0f2922 100%);
}
.comparison-item h4 {
color: #f8fafc;
margin-bottom: 10px;
}
.comparison-item .detail {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 5px;
}
.comparison-item .sub {
color: #64748b;
font-size: 0.85rem;
}
.comparison-item.highlight .detail {
color: #4ade80;
}
footer {
text-align: center;
padding: 40px 20px;
color: #64748b;
border-top: 1px solid #1e293b;
}
footer a {
color: #94a3b8;
}
@media (max-width: 700px) {
.hero h1 { font-size: 2.5rem; }
.hero .tagline { font-size: 1.2rem; }
nav .nav-links { gap: 15px; }
.features { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<nav>
<div class="nav-inner">
<a href="#" class="nav-brand">ezcal</a>
<ul class="nav-links">
<li><a href="#features">Features</a></li>
<li><a href="#examples">Examples</a></li>
<li><a href="#api">API</a></li>
<li><a href="#comparison">Comparison</a></li>
<li><a href="https://github.com/AdamPerlinski/ezcal">GitHub</a></li>
</ul>
</div>
</nav>
<section class="hero">
<h1>ezcal</h1>
<p class="tagline">Ergonomic iCalendar + vCard for Rust</p>
<p class="sub-tagline">The simplest way to read and write <code>.ics</code> and <code>.vcf</code> files. RFC 5545 + RFC 6350 in one crate.</p>
<div class="size-badge">
<span class="rfc">RFC 5545</span>
<span class="plus">+</span>
<span class="rfc">RFC 6350</span>
<span class="label">in one crate</span>
</div>
<div class="hero-buttons">
<a href="#examples" class="btn btn-primary">Quick Start</a>
<a href="https://github.com/AdamPerlinski/ezcal" class="btn btn-github">GitHub</a>
<a href="https://crates.io/crates/ezcal" class="btn btn-crates">crates.io</a>
</div>
<div class="install-box">
<span class="cmd">cargo add</span> <span class="pkg">ezcal</span>
</div>
</section>
<div class="container" id="features">
<h2 class="section-title">Features</h2>
<div class="features">
<div class="feature">
<div class="feature-icon">📅</div>
<h3>iCalendar (RFC 5545)</h3>
<p>Create and parse .ics files with events, todos, alarms, and recurrence rules. Works with Google Calendar, Apple Calendar, and Outlook.</p>
</div>
<div class="feature">
<div class="feature-icon">📇</div>
<h3>vCard (RFC 6350)</h3>
<p>Create and parse .vcf files with contacts, structured names, addresses, emails, and phone numbers. Multi-contact file support.</p>
</div>
<div class="feature">
<div class="feature-icon">🔄</div>
<h3>Round-Trip Safe</h3>
<p>Unknown properties are preserved through parse-serialize cycles. Your X-CUSTOM fields won't get lost.</p>
</div>
<div class="feature">
<div class="feature-icon">⚡</div>
<h3>Builder Pattern</h3>
<p>Ergonomic builder API lets you create events and contacts in just a few lines. No wrestling with raw strings.</p>
</div>
<div class="feature">
<div class="feature-icon">🕒</div>
<h3>chrono Integration</h3>
<p>Optional chrono feature for seamless conversion between iCal date-times and Rust's chrono types.</p>
</div>
<div class="feature">
<div class="feature-icon">🛡️</div>
<h3>Great Error Messages</h3>
<p>Parse errors include line numbers and descriptive messages. No more guessing what went wrong.</p>
</div>
</div>
</div>
<div class="container" id="examples">
<h2 class="section-title">Quick Start</h2>
<div class="examples-grid">
<div class="example-card">
<div class="example-header">
<span>📅</span>
<h3>Create an Event</h3>
</div>
<div class="example-body">
<pre><span class="keyword">use</span> <span class="type">ezcal</span>::ical::{<span class="type">Calendar</span>, <span class="type">Event</span>};
<span class="keyword">let</span> cal = <span class="type">Calendar</span>::<span class="func">new</span>()
.<span class="func">event</span>(
<span class="type">Event</span>::<span class="func">new</span>()
.<span class="func">summary</span>(<span class="string">"Team Standup"</span>)
.<span class="func">location</span>(<span class="string">"Room 42"</span>)
.<span class="func">starts</span>(<span class="string">"2026-03-15T09:00:00"</span>)
.<span class="func">ends</span>(<span class="string">"2026-03-15T09:30:00"</span>)
)
.<span class="func">build</span>();
std::fs::<span class="func">write</span>(<span class="string">"meeting.ics"</span>, cal.<span class="func">to_string</span>())<span class="punct">?</span>;</pre>
</div>
</div>
<div class="example-card">
<div class="example-header">
<span>🔍</span>
<h3>Parse an .ics File</h3>
</div>
<div class="example-body">
<pre><span class="keyword">use</span> <span class="type">ezcal</span>::ical::<span class="type">Calendar</span>;
<span class="keyword">let</span> ics = std::fs::<span class="func">read_to_string</span>(<span class="string">"meeting.ics"</span>)<span class="punct">?</span>;
<span class="keyword">let</span> calendar = <span class="type">Calendar</span>::<span class="func">parse</span>(&ics)<span class="punct">?</span>;
<span class="keyword">for</span> event <span class="keyword">in</span> calendar.<span class="func">events</span>() {
<span class="macro">println!</span>(<span class="string">"{}: {}"</span>,
event.<span class="func">get_starts</span>().<span class="func">unwrap</span>(),
event.<span class="func">get_summary</span>()
.<span class="func">unwrap_or</span>(<span class="string">"(untitled)"</span>)
);
}</pre>
</div>
</div>
<div class="example-card">
<div class="example-header">
<span>📇</span>
<h3>Create a Contact</h3>
</div>
<div class="example-body">
<pre><span class="keyword">use</span> <span class="type">ezcal</span>::vcard::<span class="type">Contact</span>;
<span class="keyword">let</span> card = <span class="type">Contact</span>::<span class="func">new</span>()
.<span class="func">full_name</span>(<span class="string">"Jane Doe"</span>)
.<span class="func">email</span>(<span class="string">"jane@example.com"</span>)
.<span class="func">phone</span>(<span class="string">"+1-555-0123"</span>)
.<span class="func">organization</span>(<span class="string">"Acme Corp"</span>)
.<span class="func">build</span>();
std::fs::<span class="func">write</span>(<span class="string">"jane.vcf"</span>, card.<span class="func">to_string</span>())<span class="punct">?</span>;</pre>
</div>
</div>
<div class="example-card">
<div class="example-header">
<span>👥</span>
<h3>Parse a .vcf File</h3>
</div>
<div class="example-body">
<pre><span class="keyword">use</span> <span class="type">ezcal</span>::vcard::<span class="type">Contact</span>;
<span class="keyword">let</span> vcf = std::fs::<span class="func">read_to_string</span>(<span class="string">"contacts.vcf"</span>)<span class="punct">?</span>;
<span class="keyword">let</span> contacts = <span class="type">Contact</span>::<span class="func">parse_all</span>(&vcf)<span class="punct">?</span>;
<span class="keyword">for</span> c <span class="keyword">in</span> &contacts {
<span class="macro">println!</span>(<span class="string">"{}: {}"</span>,
c.<span class="func">get_full_name</span>().<span class="func">unwrap</span>(),
c.<span class="func">get_email</span>()
.<span class="func">unwrap_or</span>(<span class="string">"(no email)"</span>)
);
}</pre>
</div>
</div>
</div>
</div>
<div class="container" id="api">
<div class="api-section">
<h2 class="section-title">API Reference</h2>
<div class="api-category">
<h3>iCalendar (ezcal::ical)</h3>
<table class="api-table">
<tr>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>Calendar</code></td>
<td class="desc">VCALENDAR container. Builder + parser for .ics files.</td>
</tr>
<tr>
<td><code>Event</code></td>
<td class="desc">VEVENT component. Summary, location, start/end, categories, alarms, RRULE.</td>
</tr>
<tr>
<td><code>Todo</code></td>
<td class="desc">VTODO component. Summary, due date, status, priority, percent-complete.</td>
</tr>
<tr>
<td><code>Alarm</code></td>
<td class="desc">VALARM component. Display and audio alarms with triggers.</td>
</tr>
<tr>
<td><code>RecurrenceRule</code></td>
<td class="desc">RRULE representation. Parse and serialize recurrence rules.</td>
</tr>
</table>
</div>
<div class="api-category">
<h3>vCard (ezcal::vcard)</h3>
<table class="api-table">
<tr>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>Contact</code></td>
<td class="desc">VCARD container. Builder + parser for .vcf files. Multi-contact support.</td>
</tr>
<tr>
<td><code>StructuredName</code></td>
<td class="desc">N property. Family, given, additional, prefix, suffix.</td>
</tr>
<tr>
<td><code>Address</code></td>
<td class="desc">ADR property. Street, city, region, postal code, country.</td>
</tr>
</table>
</div>
<div class="api-category">
<h3>Common (ezcal::datetime)</h3>
<table class="api-table">
<tr>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>DateTimeValue</code></td>
<td class="desc">Date/DateTime/DateTimeUtc/DateTimeTz. Parses iCal and ISO 8601 formats. Optional chrono conversion.</td>
</tr>
<tr>
<td><code>Property</code></td>
<td class="desc">Generic content-line property with name, parameters, and value.</td>
</tr>
</table>
</div>
</div>
</div>
<div class="container" id="comparison">
<h2 class="section-title">Why ezcal?</h2>
<div class="comparison">
<div class="comparison-item">
<h4>ical</h4>
<div class="detail" style="color: #f87171;">Archived</div>
<div class="sub">Read-only, archived Aug 2024</div>
</div>
<div class="comparison-item">
<h4>icalendar</h4>
<div class="detail" style="color: #fbbf24;">iCal only</div>
<div class="sub">No vCard, limited parsing</div>
</div>
<div class="comparison-item">
<h4>calcard</h4>
<div class="detail" style="color: #fbbf24;">33K LOC</div>
<div class="sub">Enterprise-grade, complex API</div>
</div>
<div class="comparison-item highlight">
<h4>ezcal</h4>
<div class="detail">Just Works</div>
<div class="sub">iCal + vCard, simple API, round-trip safe</div>
</div>
</div>
</div>
<footer>
<p>ezcal — MIT — <a href="https://github.com/AdamPerlinski/ezcal">GitHub</a> — <a href="https://crates.io/crates/ezcal">crates.io</a> — <a href="https://docs.rs/ezcal">docs.rs</a></p>
</footer>
</body>
</html>