Port canonical app to MiTerm + de-bloat terminal (redesign phase 2)

Make MiTerm the canonical app (was the stale LiveTerm template) and strip it
back to a clean, minimal terminal wired to the resume.json single source.

Port:
- Bring the evolved components/pages/utils in from the github.io fork; drop the
  old history/ folder + api_commands.ts. Import matrix theme + tailwind resolver.

De-bloat:
- Remove the CRT barrel-distortion generator, SVG filters, and crt-frame/inner
  wrappers from _app.tsx; keep only the live clock/age ticker.
- Delete the crt command (and warp/curve) and the dead .login-* overlay CSS.
- global.css trimmed to base + flat sumfetch + hover/clickable styling.

SSOT wiring:
- New src/utils/resume.ts: single typed accessor over resume.json. commands,
  sumfetch, and api read identity/content from it; config.json now holds only
  site config (identity/contact facts live once, in resume.org).
- sumfetch and ls reduced from boxed ASCII widgets to flat text: tree default
  listing, hover-highlight rows, clickable links (arXiv auto-linkified).

Verified: yarn build + tsc clean; driven headless — landing renders, live clock
ticks, help has no crt, links clickable, no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt6ycjAMdUMqGTGtbKocrs
pull/59/head
Claude 2 months ago
parent 73f1a30faa
commit a8ec735c77
No known key found for this signature in database

@ -1,21 +1,12 @@
{
"readmeUrl": "https://raw.githubusercontent.com/cveinnt/cveinnt/master/README.md",
"title": "LiveTerm",
"name": "John Doe",
"ascii": "liveterm",
"social": {
"github": "github",
"linkedin": "linkedin"
},
"email": "contact@email.com",
"ps1_hostname": "liveterm",
"ps1_username": "visitor",
"repo": "https://github.com/Cveinnt/LiveTerm",
"resume_url": "https://upload.wikimedia.org/wikipedia/commons/c/cc/Resume.pdf",
"donate_urls": {
"paypal": "https://paypal.me/cveinnt",
"patreon": "https://patreon.com/cveinnt"
},
"readmeUrl": "https://raw.githubusercontent.com/Mihir-Null/Mihir-Null/main/README.md",
"title": "Mihir Null",
"ascii": "MiTerm",
"ps1_hostname": "MiTerm",
"ps1_username": "MihirTalati",
"theme": "matrix_crt",
"repo": "https://github.com/Mihir-Null",
"resume_url": "/resume.html",
"colors": {
"light": {
"background": "#FBF1C9",

@ -0,0 +1,776 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mihir Talati Resume</title>
<style>
:root {
--content-max-width: 960px;
--primary-text: #d9ffe2;
--secondary-text: rgba(182, 255, 210, 0.75);
--accent: #9acd32;
--card-bg: rgba(4, 30, 16, 0.82);
--card-border: rgba(98, 255, 164, 0.35);
--page-bg: radial-gradient(circle at top, rgba(10, 40, 22, 0.85) 0%, rgba(1, 12, 5, 1) 55%);
--tag-bg: rgba(82, 255, 160, 0.12);
--tag-border: rgba(146, 255, 197, 0.45);
--filter-border: rgba(146, 255, 197, 0.35);
--filter-focus: rgba(154, 205, 50, 0.45);
--glass-highlight: rgba(146, 255, 197, 0.06);
}
@keyframes scanline {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'IBM Plex Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
background: #010805;
background-image: var(--page-bg);
color: var(--primary-text);
line-height: 1.65;
letter-spacing: 0.01em;
text-shadow: 0 0 6px rgba(146, 255, 197, 0.18);
position: relative;
overflow-x: hidden;
min-height: 100vh;
}
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
background: linear-gradient(
rgba(255, 255, 255, 0.04) 0%,
rgba(0, 0, 0, 0) 50%,
rgba(255, 255, 255, 0.04) 100%
);
background-size: 100% 3px;
opacity: 0.25;
mix-blend-mode: overlay;
}
body::after {
content: '';
position: fixed;
left: 0;
right: 0;
top: -100%;
height: 120%;
pointer-events: none;
background: linear-gradient(
to bottom,
rgba(154, 205, 50, 0) 0%,
rgba(154, 205, 50, 0.12) 50%,
rgba(154, 205, 50, 0) 100%
);
animation: scanline 6s linear infinite;
opacity: 0.25;
}
.container {
max-width: var(--content-max-width);
margin: 0 auto;
padding: 64px 32px 96px;
}
header {
text-align: center;
margin-bottom: 56px;
}
header h1 {
margin: 0;
font-size: 2.6rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #eafff5;
}
header p {
margin: 12px 0 0;
color: var(--secondary-text);
font-size: 1.05rem;
}
section + section {
margin-top: 56px;
}
.section-title {
font-size: 1.2rem;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 20px;
}
.card {
position: relative;
border-radius: 20px;
padding: 32px 36px;
background: var(--card-bg);
border: 1px solid var(--card-border);
box-shadow:
0 0 0 1px rgba(154, 205, 50, 0.18),
0 24px 48px rgba(10, 60, 30, 0.45);
backdrop-filter: blur(6px) saturate(120%);
}
.card::after {
content: '';
position: absolute;
inset: 12px;
border-radius: 14px;
border: 1px solid rgba(146, 255, 197, 0.08);
pointer-events: none;
}
.card + .card {
margin-top: 28px;
}
.card-header {
display: flex;
flex-wrap: wrap;
gap: 12px 16px;
align-items: baseline;
justify-content: space-between;
margin-bottom: 20px;
position: relative;
z-index: 1;
}
.card-title {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
letter-spacing: 0.04em;
color: #e5ffe9;
}
.card-subtitle {
margin: 0;
font-weight: 500;
color: var(--secondary-text);
}
.card-meta {
text-align: right;
color: var(--secondary-text);
font-size: 0.95rem;
}
.meta-line {
display: block;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 0 0 20px;
padding: 0;
list-style: none;
position: relative;
z-index: 1;
}
.tag {
display: inline-flex;
align-items: center;
padding: 5px 14px;
border-radius: 999px;
border: 1px solid var(--tag-border);
background: var(--tag-bg);
color: #9df5ba;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.02em;
box-shadow: inset 0 0 12px rgba(146, 255, 197, 0.12);
}
ul.bullet-list {
margin: 0;
padding-left: 22px;
position: relative;
z-index: 1;
}
ul.bullet-list li {
margin-bottom: 12px;
color: var(--primary-text);
}
ul.bullet-list li:last-child {
margin-bottom: 0;
}
.education-grid {
display: grid;
gap: 32px;
}
.scholarships {
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid rgba(146, 255, 197, 0.24);
}
.scholarships-title {
font-weight: 600;
margin: 0 0 10px;
color: rgba(182, 255, 210, 0.85);
letter-spacing: 0.08em;
text-transform: uppercase;
font-size: 0.75rem;
}
.courses-card {
background: linear-gradient(160deg, rgba(8, 40, 18, 0.9) 0%, rgba(2, 16, 8, 0.92) 65%, rgba(4, 24, 12, 0.96) 100%);
border-radius: 24px;
padding: 36px;
border: 1px solid rgba(146, 255, 197, 0.4);
box-shadow: 0 30px 60px rgba(6, 40, 18, 0.5);
position: relative;
overflow: hidden;
}
.courses-card::before {
content: '';
position: absolute;
inset: -60px 20% 40%;
background: radial-gradient(circle, rgba(146, 255, 197, 0.28) 0%, rgba(146, 255, 197, 0) 65%);
opacity: 0.65;
pointer-events: none;
}
.filter-label {
display: block;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
font-size: 0.75rem;
color: rgba(182, 255, 210, 0.78);
margin-bottom: 8px;
position: relative;
z-index: 1;
}
.filter-input {
width: 100%;
padding: 14px 18px;
border-radius: 12px;
border: 1px solid var(--filter-border);
font-size: 1rem;
background: rgba(2, 18, 9, 0.92);
color: var(--primary-text);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
text-shadow: inherit;
position: relative;
z-index: 1;
}
.filter-input::placeholder {
color: rgba(182, 255, 210, 0.45);
}
.filter-input:focus {
outline: none;
border-color: var(--filter-focus);
box-shadow: 0 0 0 4px rgba(154, 205, 50, 0.18);
}
.courses-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 14px;
margin-top: 26px;
position: relative;
z-index: 1;
}
.course-pill {
background: rgba(6, 28, 14, 0.9);
border-radius: 16px;
padding: 12px 16px;
border: 1px solid rgba(146, 255, 197, 0.4);
font-weight: 500;
color: var(--primary-text);
box-shadow:
inset 0 0 12px rgba(146, 255, 197, 0.08),
0 16px 32px rgba(6, 40, 18, 0.45);
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.course-pill:hover {
transform: translateY(-2px);
border-color: rgba(154, 205, 50, 0.65);
box-shadow:
inset 0 0 14px rgba(146, 255, 197, 0.12),
0 18px 36px rgba(10, 60, 30, 0.55);
}
.course-pill.hidden {
display: none;
}
.empty-state {
display: none;
margin-top: 24px;
padding: 18px;
border-radius: 14px;
background: rgba(4, 20, 10, 0.75);
border: 1px dashed rgba(146, 255, 197, 0.5);
color: rgba(212, 255, 225, 0.82);
text-align: center;
font-weight: 500;
position: relative;
z-index: 1;
}
a {
color: var(--accent);
}
@media (max-width: 720px) {
header h1 {
font-size: 2.1rem;
}
.card {
padding: 26px;
}
.card-header {
flex-direction: column;
align-items: flex-start;
}
.card-meta {
text-align: left;
}
.courses-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Mihir Talati</h1>
<p>Dual Degree · B.S. Computer Science (Honors) & B.S. Physics (Honors) · GPA 3.974</p>
</header>
<section id="education">
<h2 class="section-title">Education</h2>
<div class="education-grid">
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">University of Maryland College Park</h3>
<p class="card-subtitle">Computer Science (Honors) · Physics (Honors)</p>
</div>
<div class="card-meta">
<span class="meta-line">Expected Graduation · May 2027</span>
<span class="meta-line">Deans List · Every Semester</span>
</div>
</div>
<div class="scholarships">
<p class="scholarships-title">Scholarships & Distinctions</p>
<ul class="bullet-list">
<li>Angelo Bardasis Scholarship (Physics)</li>
<li>John D. Gannon Scholarship (Computer Science)</li>
</ul>
</div>
</article>
</div>
</section>
<section id="work">
<h2 class="section-title">Work Experience</h2>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Researcher · CAAR REU</h3>
<p class="card-subtitle">Combinatorics, Algorithms, and AI for Real Problems</p>
</div>
<div class="card-meta">
<span class="meta-line">Jun 2025 Aug 2025</span>
<span class="meta-line">QuICS · University of Maryland</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">Python</li>
<li class="tag">Galois</li>
<li class="tag">NumPy</li>
<li class="tag">Magma Math</li>
<li class="tag">SageMath</li>
</ul>
<ul class="bullet-list">
<li>Designed novel fault-tolerant CSS quantum error-correcting codes for qudits inspired by quadratic residue codes.</li>
<li>Established code families with transversal Clifford and T gate support through finite field and QEC theory proofs.</li>
<li>Implemented parallel Python tooling to validate self-orthogonality and distance properties across large search spaces.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Undergraduate Researcher · Bright Beams Collective</h3>
<p class="card-subtitle">IREAP Research Experience for Undergraduates</p>
</div>
<div class="card-meta">
<span class="meta-line">Sep 2023 Dec 2024</span>
<span class="meta-line">Institute for Research in Electronics and Applied Physics</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">MAD-X</li>
<li class="tag">Elegant</li>
<li class="tag">Fusion 360</li>
<li class="tag">MATLAB</li>
<li class="tag">Ansys Workbench</li>
</ul>
<ul class="bullet-list">
<li>Built custom machine learning and gradient-based optimizers to derive design parameters for a planned beamline.</li>
<li>Ran accelerator simulations to analyze lattice stability and beam quality under varied operating configurations.</li>
<li>Produced animations of betatron motion for outreach presentations and technical discussions.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Software Engineering Intern · JODO PAY</h3>
<p class="card-subtitle">Invoicing System Automation</p>
</div>
<div class="card-meta">
<span class="meta-line">Jun 2022 Jul 2022</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">JavaScript</li>
<li class="tag">TypeScript</li>
<li class="tag">Google Apps Script</li>
</ul>
<ul class="bullet-list">
<li>Automated invoice generation and dispatch for clients, reducing manual processing by 40 staff hours each week.</li>
<li>Integrated Google Workspace APIs to manage templates, customer data, and error handling workflows.</li>
</ul>
</article>
</section>
<section id="projects">
<h2 class="section-title">Projects</h2>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Quantum State Tomography via QML</h3>
<p class="card-subtitle">GitHub · github.com/Mihir-Null</p>
</div>
<div class="card-meta">
<span class="meta-line">Sep 2024 Jul 2025</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">Python</li>
<li class="tag">Qiskit</li>
<li class="tag">Aer</li>
<li class="tag">MPI</li>
<li class="tag">Numba</li>
<li class="tag">PyTorch</li>
</ul>
<ul class="bullet-list">
<li>Co-developed a variational circuit tomography approach, exploring loss landscapes, embedding strategies, and optimizers.</li>
<li>Executed 2,000+ experiments across multi-GPU clusters with a custom orchestrator for reproducible benchmarking.</li>
<li>Co-authored <em>Quantum state reconstruction with variational quantum circuit</em> (arXiv:2507.01246).</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">MIT iQuHack · QuEra Challenge</h3>
<p class="card-subtitle">Honorable Mention</p>
</div>
<div class="card-meta">
<span class="meta-line">Feb 2025</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">Python</li>
<li class="tag">Qiskit</li>
<li class="tag">Cirq</li>
<li class="tag">Bloqade</li>
<li class="tag">PyTorch</li>
<li class="tag">TensorFlow Quantum</li>
</ul>
<ul class="bullet-list">
<li>Designed a compilation pipeline mapping diverse quantum circuits—including QAOA and Steane codes—to QuEra hardware.</li>
<li>Optimized pulse sequences for neutral atom instruction sets, contributing to the teams honorable mention finish.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Quantum Machine Learning Research</h3>
<p class="card-subtitle">Undergraduate Research Portfolio</p>
</div>
<div class="card-meta">
<span class="meta-line">Aug 2023 Dec 2024</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">Python</li>
<li class="tag">Qiskit</li>
<li class="tag">Cirq</li>
<li class="tag">PennyLane</li>
<li class="tag">Q#</li>
</ul>
<ul class="bullet-list">
<li>Implemented quantum image processing with parameterized circuit kernels integrated into CNN architectures.</li>
<li>Created a hybrid transitional optimizer to mitigate barren plateaus in QAOA, presented at the FYIRE Summit 2024.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Terrapin Rocket Team · Kalman Filters</h3>
<p class="card-subtitle">Guidance, Navigation, and Control</p>
</div>
<div class="card-meta">
<span class="meta-line">Aug 2023 Jan 2024</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">C</li>
<li class="tag">C++</li>
<li class="tag">MATLAB</li>
</ul>
<ul class="bullet-list">
<li>Developed six- and nine-state Kalman filters fusing multi-sensor telemetry for rocket position estimation.</li>
<li>Supported the teams winning entry at the 2024 Spaceport America Cup.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Robotics @ UMD · Computer Vision</h3>
<p class="card-subtitle">Testudog Automation</p>
</div>
<div class="card-meta">
<span class="meta-line">Aug 2023 Jan 2024</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">Python</li>
<li class="tag">OpenCV</li>
<li class="tag">scikit-learn</li>
</ul>
<ul class="bullet-list">
<li>Developed Haar cascade pipelines for the Testudog robotics platform deployed on a Jetson-powered Boston Dynamics Spot.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Club Administration Automation</h3>
<p class="card-subtitle">Workflow Automation Suite</p>
</div>
<div class="card-meta">
<span class="meta-line">Nov 2024 Feb 2025</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">Python</li>
<li class="tag">Selenium</li>
<li class="tag">Discord.py</li>
<li class="tag">REST APIs</li>
<li class="tag">OAuth 2.0</li>
</ul>
<ul class="bullet-list">
<li>Automated Discord announcements, email campaigns, Instagram publishing, and room reservations for university clubs.</li>
<li>Currently deployed across multiple organizations to streamline administrative overhead.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Housing Market Analysis</h3>
<p class="card-subtitle">Predictive Pricing Models</p>
</div>
<div class="card-meta">
<span class="meta-line">Jun 2024 Jul 2024</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">Python</li>
<li class="tag">Pandas</li>
<li class="tag">SciPy</li>
<li class="tag">NumPy</li>
<li class="tag">scikit-learn</li>
<li class="tag">SQL</li>
</ul>
<ul class="bullet-list">
<li>Trained feedforward neural networks for price prediction using engineered features derived from public listings and census data.</li>
<li>Applied PCA and statistical analysis to identify high-signal attributes and reduce overfitting.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Independent Game Development</h3>
<p class="card-subtitle">National Game Jam Winner</p>
</div>
<div class="card-meta">
<span class="meta-line">2020 2023</span>
</div>
</div>
<ul class="tag-list">
<li class="tag">C#</li>
<li class="tag">C++</li>
<li class="tag">Unity</li>
<li class="tag">Unreal Engine</li>
<li class="tag">Blender</li>
<li class="tag">Adobe Suite</li>
</ul>
<ul class="bullet-list">
<li>Published two action platformers, including a custom 2D soft-body simulation available via Two Guys Studios on itch.io.</li>
<li>Placed second in the 2021 Mindbox National Design Championship; finalist in 2020 edition.</li>
<li>Built a procedural map generation toolkit leveraging noise algorithms and custom pathfinding.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Author · A Very Quick Guide to Special Relativity</h3>
<p class="card-subtitle">Educational Publication</p>
</div>
<div class="card-meta">
<span class="meta-line">Sep 2020 Jun 2021</span>
</div>
</div>
<ul class="bullet-list">
<li>Authored and published an accessible introduction to special relativity covering time dilation through massenergy equivalence.</li>
</ul>
</article>
</section>
<section id="organizations">
<h2 class="section-title">Organizations</h2>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Industry Chair · Quantum Coalition</h3>
</div>
<div class="card-meta">
<span class="meta-line">Jan 2024 Present</span>
</div>
</div>
<ul class="bullet-list">
<li>Co-organized the QRISE research competition with 200+ international participants.</li>
<li>Partnered with the UN and ITU to deliver the QC-FLIQ hackathon for the International Year of Quantum (500+ attendees).</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">President · Undergraduate Quantum Association</h3>
</div>
<div class="card-meta">
<span class="meta-line">Jan 2024 Present</span>
</div>
</div>
<ul class="bullet-list">
<li>Led organizational strategy, programming, and outreach after serving as officer since 2020.</li>
<li>Produced the Quantum Leap Career Nexus with 240+ attendees and partners including IBM, Microsoft, and IonQ.</li>
</ul>
</article>
<article class="card">
<div class="card-header">
<div>
<h3 class="card-title">Co-President · ACM@UMD</h3>
</div>
<div class="card-meta">
<span class="meta-line">Sep 2023 Present</span>
</div>
</div>
<ul class="bullet-list">
<li>Restarted the university chapter, launched a new website, and grew membership beyond 145 students.</li>
<li>Led workshops focused on shell scripting, AI, quantum computing, and heuristic search techniques.</li>
</ul>
</article>
</section>
<section id="courses">
<h2 class="section-title">Relevant Courses</h2>
<div class="courses-card">
<label class="filter-label" for="course-filter">Search Courses</label>
<input class="filter-input" id="course-filter" type="search" placeholder="Start typing to filter by course or code…" />
<div class="courses-grid" id="courses-grid">
<span class="course-pill">Graduate Quantum Computing (CMSC657)</span>
<span class="course-pill">Computer Science Honors Seminar</span>
<span class="course-pill">Differentiable Programming for Agentic AI (CMSC498Z)</span>
<span class="course-pill">Natural Language Processing (CMSC470)</span>
<span class="course-pill">Data Science (CMSC320)</span>
<span class="course-pill">Design & Analysis of Algorithms (CMSC451)</span>
<span class="course-pill">Object-Oriented Programming in Java (CMSC132)</span>
<span class="course-pill">Applied Probability & Statistics (STAT400)</span>
<span class="course-pill">Honors Advanced Modern Physics (PHYS400)</span>
<span class="course-pill">Quantum Mechanics II (PHYS402)</span>
<span class="course-pill">Multivariate Calculus</span>
<span class="course-pill">Differential Equations</span>
</div>
<div class="empty-state" id="courses-empty">No courses match that search. Try a broader keyword.</div>
</div>
</section>
</div>
<script>
const courseFilterInput = document.getElementById('course-filter');
const coursePills = Array.from(document.querySelectorAll('.course-pill'));
const emptyState = document.getElementById('courses-empty');
function applyCourseFilter(value) {
const query = value.trim().toLowerCase();
let visibleCount = 0;
coursePills.forEach((pill) => {
const matches = pill.textContent.toLowerCase().includes(query);
pill.classList.toggle('hidden', !matches);
if (matches) visibleCount += 1;
});
emptyState.style.display = visibleCount === 0 ? 'block' : 'none';
}
courseFilterInput.addEventListener('input', (event) => {
applyCourseFilter(event.target.value);
});
</script>
</body>
</html>

@ -134,7 +134,8 @@ function parse(text) {
flushSkill();
const key = slug(label);
section = SECTIONS[key] ? { key, cfg: SECTIONS[key] } : null;
if (!section) process.stderr.write(`warn: unknown section "${label}"\n`);
if (!section)
process.stderr.write(`warn: unknown section "${label}"\n`);
} else if (level === 2 && section) {
if (section.cfg.kind === 'items') {
flushItem();
@ -150,8 +151,10 @@ function parse(text) {
if (t.startsWith('- ')) {
const val = t.slice(2).trim();
if (section && section.cfg.kind === 'items' && itemBullets) itemBullets.push(val);
else if (section && section.cfg.kind === 'skills' && skillCat) skillCat.list.push(val);
if (section && section.cfg.kind === 'items' && itemBullets)
itemBullets.push(val);
else if (section && section.cfg.kind === 'skills' && skillCat)
skillCat.list.push(val);
continue;
}

@ -0,0 +1,49 @@
import React from 'react';
import { Ps1 } from './Ps1';
export interface HistoryEntry {
id: number;
date: Date;
command: string;
output: string;
}
export const useHistory = (defaultValue: HistoryEntry[]) => {
const [history, setHistory] = React.useState<HistoryEntry[]>(defaultValue);
const [command, setCommand] = React.useState('');
const [lastCommandIndex, setLastCommandIndex] = React.useState(0);
return {
history,
command,
lastCommandIndex,
setHistory: (value: string) =>
setHistory([
...history,
{ id: history.length, date: new Date(), command, output: value },
]),
setCommand,
setLastCommandIndex,
clearHistory: () => setHistory([]),
};
};
export const History: React.FC<{ history: HistoryEntry[] }> = ({ history }) => (
<>
{history.map((entry, index) => (
<div key={entry.command + index}>
<div className="flex flex-row space-x-2">
<div className="flex-shrink">
<Ps1 />
</div>
<div className="flex-grow">{entry.command}</div>
</div>
<p
className="whitespace-pre-wrap mb-2"
style={{ lineHeight: 'normal' }}
dangerouslySetInnerHTML={{ __html: entry.output }}
/>
</div>
))}
</>
);

@ -1,17 +1,14 @@
import React from 'react';
import config from '../../config.json';
export const Ps1 = () => {
export const Ps1: React.FC<{ username?: string }> = ({ username }) => {
const name = username && username.length > 0 ? username : config.ps1_username;
return (
<div>
<span className="text-light-yellow dark:text-dark-yellow">
{config.ps1_username}
</span>
<span className="text-light-gray dark:text-dark-gray">@</span>
<span className="text-light-green dark:text-dark-green">
{config.ps1_hostname}
</span>
<span className="text-light-gray dark:text-dark-gray">:$ ~ </span>
<span className="text-light-yellow dark:text-dark-yellow">{name}</span>
<span className="ps1-separator">@</span>
<span className="ps1-host">{config.ps1_hostname}</span>
<span className="ps1-path">:$ ~ </span>
</div>
);
};

@ -1,31 +0,0 @@
import React from 'react';
import { History as HistoryInterface } from './interface';
import { Ps1 } from '../Ps1';
export const History: React.FC<{ history: Array<HistoryInterface> }> = ({
history,
}) => {
return (
<>
{history.map((entry: HistoryInterface, index: number) => (
<div key={entry.command + index}>
<div className="flex flex-row space-x-2">
<div className="flex-shrink">
<Ps1 />
</div>
<div className="flex-grow">{entry.command}</div>
</div>
<p
className="whitespace-pre-wrap mb-2"
style={{ lineHeight: 'normal' }}
dangerouslySetInnerHTML={{ __html: entry.output }}
/>
</div>
))}
</>
);
};
export default History;

@ -1,27 +0,0 @@
import React from 'react';
import { History } from './interface';
export const useHistory = (defaultValue: Array<History>) => {
const [history, setHistory] = React.useState<Array<History>>(defaultValue);
const [command, setCommand] = React.useState<string>('');
const [lastCommandIndex, setLastCommandIndex] = React.useState<number>(0);
return {
history,
command,
lastCommandIndex,
setHistory: (value: string) =>
setHistory([
...history,
{
id: history.length,
date: new Date(),
command,
output: value,
},
]),
setCommand,
setLastCommandIndex,
clearHistory: () => setHistory([]),
};
};

@ -1,6 +0,0 @@
export interface History {
id: number;
date: Date;
command: string;
output: string;
}

@ -14,9 +14,21 @@ export const Input = ({
setHistory,
setLastCommandIndex,
clearHistory,
username,
}: {
inputRef: React.MutableRefObject<HTMLInputElement>;
containerRef: React.MutableRefObject<any>;
command: string;
history: any[];
lastCommandIndex: number;
setCommand: (v: string) => void;
setHistory: (v: string) => void;
setLastCommandIndex: (v: number) => void;
clearHistory: () => void;
username?: string;
}) => {
const onSubmit = async (event: React.KeyboardEvent<HTMLInputElement>) => {
const commands: [string] = history
const commands: string[] = history
.map(({ command }) => command)
.filter((command: string) => command);
@ -79,21 +91,22 @@ export const Input = ({
};
return (
<div className="flex flex-row space-x-2">
<div className="terminal-current-line flex flex-row space-x-2">
<label htmlFor="prompt" className="flex-shrink">
<Ps1 />
<Ps1 username={username} />
</label>
<input
ref={inputRef}
id="prompt"
type="text"
className={`bg-light-background dark:bg-dark-background focus:outline-none flex-grow ${
className={`bg-transparent focus:outline-none flex-grow ${
commandExists(command) || command === ''
? 'text-dark-green'
: 'text-dark-red'
}`}
value={command}
placeholder="try help, ls, resume, or summary"
onChange={onChange}
autoFocus
onKeyDown={onSubmit}

@ -1,23 +1,74 @@
import React from 'react';
import '../styles/global.css';
import Head from 'next/head';
import config from '../../config.json';
const App = ({ Component, pageProps }) => {
const inputRef = React.useRef<HTMLInputElement>(null);
const onClickAnywhere = () => {
inputRef.current.focus();
inputRef.current?.focus();
};
// Live updates for the sumfetch datetime and age fields.
React.useEffect(() => {
const birth = new Date(2005, 6, 26); // 26 July 2005 (month is 0-based)
const pad = (n: number) => String(n).padStart(2, '0');
const fmtNow = () => {
const n = new Date();
return `${n.getFullYear()}-${pad(n.getMonth() + 1)}-${pad(
n.getDate(),
)} ${pad(n.getHours())}:${pad(n.getMinutes())}:${pad(n.getSeconds())}`;
};
const computeAge = () => {
const n = new Date();
let years = n.getFullYear() - birth.getFullYear();
const anniv = new Date(
n.getFullYear(),
birth.getMonth(),
birth.getDate(),
);
if (n < anniv) years -= 1;
let lastBD = new Date(n.getFullYear(), birth.getMonth(), birth.getDate());
if (n < lastBD)
lastBD = new Date(
n.getFullYear() - 1,
birth.getMonth(),
birth.getDate(),
);
const days = Math.floor((n.getTime() - lastBD.getTime()) / 86400000);
return `${years}y ${days}d`;
};
const tick = () => {
document.querySelectorAll('[data-sum-dt]').forEach((el) => {
(el as HTMLElement).textContent = fmtNow();
});
document.querySelectorAll('[data-sum-age]').forEach((el) => {
(el as HTMLElement).textContent = computeAge();
});
};
tick();
const id = window.setInterval(tick, 1000);
return () => window.clearInterval(id);
}, []);
return (
<>
<Head>
<title>{config.title}</title>
<meta
name="viewport"
content="initial-scale=1.0, width=device-width"
key="viewport"
maximum-scale="1"
/>
<link
rel="preload"
href="/assets/fonts/Hack-NF.ttf"
as="font"
type="font/ttf"
crossOrigin="anonymous"
/>
</Head>
<div

@ -2,9 +2,8 @@ import Head from 'next/head';
import React from 'react';
import config from '../../config.json';
import { Input } from '../components/input';
import { useHistory } from '../components/history/hook';
import { History } from '../components/history/History';
import { banner } from '../utils/bin';
import { History, useHistory } from '../components/History';
import { sumfetch } from '../utils/bin';
interface IndexPageProps {
inputRef: React.MutableRefObject<HTMLInputElement>;
@ -22,7 +21,9 @@ const IndexPage: React.FC<IndexPageProps> = ({ inputRef }) => {
setLastCommandIndex,
} = useHistory([]);
const init = React.useCallback(() => setHistory(banner()), []);
const init = React.useCallback(() => {
sumfetch([]).then((out) => setHistory(out));
}, []);
React.useEffect(() => {
init();
@ -41,10 +42,9 @@ const IndexPage: React.FC<IndexPageProps> = ({ inputRef }) => {
<title>{config.title}</title>
</Head>
<div className="p-8 overflow-hidden h-full border-2 rounded border-light-yellow dark:border-dark-yellow">
<div className="p-4 md:p-8 overflow-hidden h-full">
<div ref={containerRef} className="overflow-y-auto h-full">
<History history={history} />
<Input
inputRef={inputRef}
containerRef={containerRef}
@ -58,6 +58,15 @@ const IndexPage: React.FC<IndexPageProps> = ({ inputRef }) => {
/>
</div>
</div>
<a
href={config.resume_url}
target="_blank"
rel="noreferrer"
className="resume-fab"
>
resume
</a>
</>
);
};

@ -27,15 +27,109 @@ div#__next > div {
}
::-webkit-scrollbar-track {
background: #1e252e;
background: rgba(0, 0, 0, 0.15);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: #ebdbb2;
background: rgba(154, 205, 50, 0.5);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #ff8037;
background: rgba(154, 205, 50, 0.75);
}
/* --- Prompt line --- */
.terminal-current-line {
padding: 2px 0;
}
.ps1-separator {
color: rgba(154, 205, 50, 0.85);
margin: 0 2px;
}
.ps1-host {
color: #a4ffce;
}
.ps1-path {
color: rgba(186, 255, 216, 0.85);
margin-left: 4px;
}
/* --- Clickable elements, modern-terminal style: underline on hover --- */
.term-link {
color: inherit;
text-decoration: none;
border-bottom: 1px dotted currentColor;
cursor: pointer;
}
.term-link:hover {
text-decoration: none;
border-bottom-style: solid;
filter: brightness(1.25);
}
/* --- Tree listing (ls) --- */
.tree {
white-space: pre-wrap;
}
.tree-branch {
color: rgba(154, 205, 50, 0.65);
}
.tree-row {
display: inline;
padding: 0 3px;
border-radius: 3px;
}
.tree-row:hover {
background: rgba(154, 205, 50, 0.12);
}
/* --- sumfetch: flat text summary --- */
.sumfetch {
white-space: pre-wrap;
margin: 0;
line-height: 1.35;
}
.sf-name {
font-weight: 700;
font-size: 1.05em;
color: #b6ff66;
}
.sf-title {
color: rgba(154, 205, 50, 0.85);
font-weight: 700;
}
.sf-key {
color: #9eff88;
}
.sf-link {
color: inherit;
text-decoration: none;
border-bottom: 1px dotted currentColor;
}
.sf-link:hover {
border-bottom-style: solid;
filter: brightness(1.25);
}
/* --- Floating resume button --- */
.resume-fab {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 50;
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
border: 1px solid rgba(154, 205, 50, 0.6);
color: rgba(186, 255, 216, 0.9);
text-decoration: none;
transition: background-color 0.15s ease, color 0.15s ease;
}
.resume-fab:hover {
background: rgba(154, 205, 50, 0.85);
color: #06210f;
}

@ -1,9 +1,10 @@
import axios from 'axios';
import config from '../../config.json';
import { profile } from './resume';
export const getProjects = async () => {
const { data } = await axios.get(
`https://api.github.com/users/${config.social.github}/repos`,
`https://api.github.com/users/${profile.github}/repos`,
);
return data;
};

@ -1,36 +0,0 @@
// // List of commands that require API calls
import { getProjects } from '../api';
import { getQuote } from '../api';
import { getReadme } from '../api';
import { getWeather } from '../api';
export const projects = async (args: string[]): Promise<string> => {
const projects = await getProjects();
return projects
.map(
(repo) =>
`${repo.name} - <a class="text-light-blue dark:text-dark-blue underline" href="${repo.html_url}" target="_blank">${repo.html_url}</a>`,
)
.join('\n');
};
export const quote = async (args: string[]): Promise<string> => {
const data = await getQuote();
return data.quote;
};
export const readme = async (args: string[]): Promise<string> => {
const readme = await getReadme();
return `Opening GitHub README...\n
${readme}`;
};
export const weather = async (args: string[]): Promise<string> => {
const city = args.join('+');
if (!city) {
return 'Usage: weather [city]. Example: weather casablanca';
}
const weather = await getWeather(city);
return weather;
};

@ -1,76 +1,175 @@
// List of commands that do not require API calls
import * as bin from './index';
import config from '../../../config.json';
import {
profile,
experience,
projects,
education,
organizations,
publications,
skills,
catLabel,
} from '../resume';
import { getProjects, getQuote, getReadme, getWeather } from '../api';
const b = (s: string) => `${s}`;
const link = (href: string, text: string) =>
`<a class="term-link" href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
// Turn "arXiv:2507.01246" mentions into a clickable link, in place.
const linkifyArxiv = (s: string) =>
s.replace(/arXiv:(\d{4}\.\d{4,5})/gi, (_m, id) =>
link(`https://arxiv.org/abs/${id}`, `arXiv:${id}`),
);
export const help = async (_args: string[]): Promise<string> => {
const cmds: [string, string][] = [
['about', 'short bio'],
[
'ls [section]',
'browse résumé — experience projects education skills organizations publications',
],
['summary', 'full text profile'],
['sumfetch', 'profile card'],
['resume', 'open résumé page in a new tab'],
['email', 'open mailto link'],
['linkedin', 'open LinkedIn profile'],
['github', 'open GitHub profile'],
['github_projects', 'list public GitHub repos'],
['weather [city]', 'current weather'],
['quote', 'random quote'],
['clear', 'clear the terminal'],
];
const rows = cmds
.map(
([cmd, desc], i) =>
`${i === cmds.length - 1 ? '└' : '├'} ${cmd.padEnd(18)}${desc}`,
)
.join('\n');
return `Commands:\n${rows}\n\n[tab] complete [ctrl+l] clear [↑/↓] history`;
};
export const about = async (_args: string[]): Promise<string> => {
return `${profile.name}\n${profile.bio}\n\ntype 'sumfetch' for the profile card, 'ls' to browse sections`;
};
const SECTIONS = [
['experience', 'professional history'],
['projects', 'things built'],
['education', 'degrees & coursework'],
['organizations', 'leadership roles'],
['publications', 'papers & writing'],
['skills', 'technical stack'],
] as const;
// Help
export const help = async (args: string[]): Promise<string> => {
const commands = Object.keys(bin).sort().join(', ');
var c = '';
for (let i = 1; i <= Object.keys(bin).sort().length; i++) {
if (i % 7 === 0) {
c += Object.keys(bin).sort()[i - 1] + '\n';
} else {
c += Object.keys(bin).sort()[i - 1] + ' ';
}
export const ls = async (args: string[]): Promise<string> => {
const section = (args[0] ?? '').toLowerCase().replace(/\/$/, '');
if (!section) {
const rows = SECTIONS.map(([name, desc], i) => {
const branch = i === SECTIONS.length - 1 ? '└' : '├';
return `<span class="tree-branch">${branch}</span> <span class="tree-row">${name.padEnd(
15,
)}${desc}</span>`;
}).join('\n');
return `<span class="tree">~/\n${rows}</span>\n\nuse 'ls &lt;section&gt;' to read, e.g. 'ls experience'`;
}
return `Welcome! Here are all the available commands:
\n${c}\n
[tab]: trigger completion.
[ctrl+l]/clear: clear terminal.\n
Type 'sumfetch' to display summary.
`;
};
// Redirection
export const repo = async (args: string[]): Promise<string> => {
window.open(`${config.repo}`);
return 'Opening Github repository...';
};
// About
export const about = async (args: string[]): Promise<string> => {
return `Hi, I am ${config.name}.
Welcome to my website!
More about me:
'sumfetch' - short summary.
'resume' - my latest resume.
'readme' - my github readme.`;
switch (section) {
case 'about':
return `${profile.name}\n${profile.bio}`;
case 'experience':
return experience
.map(
(e) =>
`${e.role} @ ${e.company} (${e.dates})\n${e.bullets
.map(b)
.join('\n')}`,
)
.join('\n\n');
case 'projects':
return projects
.map(
(p) =>
`${p.name} [${p.timeline}]\n${p.bullets
.map((x) => b(linkifyArxiv(x)))
.join('\n')}`,
)
.join('\n\n');
case 'education':
return education
.map((e) =>
[
e.school,
e.degrees.map((d) => ` ${d}`).join('\n'),
` GPA: ${e.gpa} · Graduation: ${e.graduation}`,
'',
'Honors:',
e.honors.map(b).join('\n'),
'',
'Coursework:',
e.coursework.map(b).join('\n'),
].join('\n'),
)
.join('\n\n');
case 'organizations':
return organizations
.map(
(o) =>
`${o.role} @ ${o.org} (${o.dates})\n${o.bullets
.map(b)
.join('\n')}`,
)
.join('\n\n');
case 'publications':
return publications
.map(
(p) =>
`${p.title}\n ${linkifyArxiv(p.status)}\n${p.bullets
.map((x) => b(linkifyArxiv(x)))
.join('\n')}`,
)
.join('\n\n');
case 'skills':
return Object.entries(skills)
.map(
([cat, items]) =>
`${(catLabel(cat) + ':').padEnd(13)}${items.join(', ')}`,
)
.join('\n');
default:
return `ls: '${section}' not found\nSections: ${SECTIONS.map(
([n]) => n,
).join(', ')}`;
}
};
export const resume = async (args: string[]): Promise<string> => {
window.open(`${config.resume_url}`);
return 'Opening resume...';
export const resume = async (_args: string[]): Promise<string> => {
window.open(config.resume_url);
return 'Opening résumé...';
};
// Donate
export const donate = async (args: string[]): Promise<string> => {
return `thank you for your interest.
here are the ways you can support my work:
- <u><a class="text-light-blue dark:text-dark-blue underline" href="${config.donate_urls.paypal}" target="_blank">paypal</a></u>
- <u><a class="text-light-blue dark:text-dark-blue underline" href="${config.donate_urls.patreon}" target="_blank">patreon</a></u>
`;
export const email = async (_args: string[]): Promise<string> => {
window.open(`mailto:${profile.email}`);
return `Opening mailto:${profile.email}...`;
};
// Contact
export const email = async (args: string[]): Promise<string> => {
window.open(`mailto:${config.email}`);
return `Opening mailto:${config.email}...`;
export const github = async (_args: string[]): Promise<string> => {
window.open(`https://github.com/${profile.github}/`);
return 'Opening GitHub...';
};
export const github = async (args: string[]): Promise<string> => {
window.open(`https://github.com/${config.social.github}/`);
return 'Opening github...';
};
export const linkedin = async (args: string[]): Promise<string> => {
window.open(`https://www.linkedin.com/in/${config.social.linkedin}/`);
return 'Opening linkedin...';
export const linkedin = async (_args: string[]): Promise<string> => {
window.open(`https://www.linkedin.com/in/${profile.linkedin}/`);
return 'Opening LinkedIn...';
};
// Search
export const google = async (args: string[]): Promise<string> => {
window.open(`https://google.com/search?q=${args.join(' ')}`);
return `Searching google for ${args.join(' ')}...`;
@ -81,77 +180,54 @@ export const duckduckgo = async (args: string[]): Promise<string> => {
return `Searching duckduckgo for ${args.join(' ')}...`;
};
export const bing = async (args: string[]): Promise<string> => {
window.open(`https://bing.com/search?q=${args.join(' ')}`);
return `Wow, really? You are using bing for ${args.join(' ')}?`;
};
export const reddit = async (args: string[]): Promise<string> => {
window.open(`https://www.reddit.com/search/?q=${args.join(' ')}`);
return `Searching reddit for ${args.join(' ')}...`;
};
// Typical linux commands
export const echo = async (args: string[]): Promise<string> => {
return args.join(' ');
};
export const whoami = async (args: string[]): Promise<string> => {
return `${config.ps1_username}`;
};
export const echo = async (args: string[]): Promise<string> => args.join(' ');
export const ls = async (args: string[]): Promise<string> => {
return `a
bunch
of
fake
directories`;
};
export const whoami = async (_args: string[]): Promise<string> =>
config.ps1_username;
export const cd = async (args: string[]): Promise<string> => {
return `unfortunately, i cannot afford more directories.
if you want to help, you can type 'donate'.`;
};
export const date = async (_args: string[]): Promise<string> =>
new Date().toString();
export const date = async (args: string[]): Promise<string> => {
return new Date().toString();
};
export const vi = async (_args: string[]): Promise<string> =>
`woah, you still use 'vi'? just try 'vim'.`;
export const vim = async (_args: string[]): Promise<string> =>
`'vim' is so outdated. how about 'nvim'?`;
export const nvim = async (_args: string[]): Promise<string> =>
`'nvim'? too fancy. why not 'emacs'?`;
export const emacs = async (_args?: string[]): Promise<string> =>
`you know what? just use vscode.`;
export const vi = async (args: string[]): Promise<string> => {
return `woah, you still use 'vi'? just try 'vim'.`;
export const sudo = async (_args?: string[]): Promise<string> => {
window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '_blank');
return `Permission denied: with little power comes... no responsibility?`;
};
export const vim = async (args: string[]): Promise<string> => {
return `'vim' is so outdated. how about 'nvim'?`;
};
// --- API commands ---
export const nvim = async (args: string[]): Promise<string> => {
return `'nvim'? too fancy. why not 'emacs'?`;
export const github_projects = async (_args: string[]): Promise<string> => {
const repos = await getProjects();
return repos
.map((repo: any) => `${repo.name}${link(repo.html_url, repo.html_url)}`)
.join('\n');
};
export const emacs = async (args?: string[]): Promise<string> => {
return `you know what? just use vscode.`;
export const quote = async (_args: string[]): Promise<string> => {
const data = await getQuote();
return data.quote;
};
export const sudo = async (args?: string[]): Promise<string> => {
window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '_blank'); // ...I'm sorry
return `Permission denied: with little power comes... no responsibility? `;
export const readme = async (_args: string[]): Promise<string> => {
const data = await getReadme();
return `Opening GitHub README...\n\n${data}`;
};
// Banner
export const banner = (args?: string[]): string => {
return `
Type 'help' to see the list of available commands.
Type 'sumfetch' to display summary.
Type 'repo' or click <u><a class="text-light-blue dark:text-dark-blue underline" href="${config.repo}" target="_blank">here</a></u> for the Github repository.
`;
export const weather = async (args: string[]): Promise<string> => {
const city = args.join('+');
if (!city) return 'Usage: weather [city]. Example: weather london';
return getWeather(city);
};

@ -1,3 +1,2 @@
export * from './commands';
export * from './api_commands';
export { default as sumfetch } from './sumfetch';
export * from './sumfetch';

@ -1,46 +1,152 @@
import config from '../../../config.json';
import {
profile,
experience,
projects,
education,
organizations,
publications,
skills,
catLabel,
} from '../resume';
const sumfetch = async (args: string[]): Promise<string> => {
if (config.ascii === 'cveinnt') {
return `
@@@@@@@@@@@@@ sumfetch: summary display
@@@@ @@@@ -----------
@@ @@ ABOUT
@@ @@ ${config.name}
@@ @@ ${config.ps1_hostname}
@@ @@@ @@ <u><a href="${config.resume_url}" target="_blank">resume</a></u>
@@ @@@ @@ <u><a href="${config.repo}" target="_blank">Github repo</a></u>
@@ @@ -----------
@@ .@@@@@@@@@@. @@ CONTACT
@@ @@ @@ @@ <u><a href="mailto:${config.email}" target="_blank">${config.email}</a></u>
@@ @@ @@ @@ <u><a href="https://github.com/${config.social.github}" target="_blank">github.com/${config.social.github}</a></u>
@@ @@@@@@ @@ <u><a href="https://linkedin.com/in/${config.social.linkedin}" target="_blank">linkedin.com/in/${config.social.linkedin}</a></u>
@@@ @@@ -----------
@@@ @@@ @@ DONATE
@| @@@@@@@@@@@@@@@@ @@ <u><a href="${config.donate_urls.paypal}" target="_blank">${config.donate_urls.paypal}</a></u>
@| @@ <u><a href="${config.donate_urls.patreon}" target="_blank">${config.donate_urls.patreon}</a></u>
`;
} else {
return `
  sumfetch
    -----------
      ABOUT
  ${config.name}
        <u><a href="${config.resume_url}" target="_blank">resume</a></u>
<u><a href="${config.repo}" target="_blank">Github repo</a></u>
  -----------
  > L I V E T E R M CONTACT
  <u><a href="mailto:${config.email}" target="_blank">${config.email}</a></u>
<u><a href="https://github.com/${config.social.github}" target="_blank">github.com/${config.social.github}</a></u>
        <u><a href="https://linkedin.com/in/${config.social.linkedin}" target="_blank">linkedin.com/in/${config.social.linkedin}</a></u>
  -----------
        DONATE
   <u><a href="${config.donate_urls.paypal}" target="_blank">${config.donate_urls.paypal}</a></u>
  <u><a href="${config.donate_urls.patreon}" target="_blank">${config.donate_urls.patreon}</a></u>
`;
}
const key = (k: string, w = 11) => `<span class="sf-key">${k.padEnd(w)}</span>`;
const title = (t: string) => `<span class="sf-title">${t}</span>`;
const sfLink = (href: string, text: string) =>
`<a class="sf-link" href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
// --- sumfetch: clean flat profile card (the landing screen) ---
const sumfetch = async (_args: string[]): Promise<string> => {
const edu = education[0] ?? ({} as any);
const contact = [
` ${key('email')}${sfLink(`mailto:${profile.email}`, profile.email)}`,
` ${key('github')}${sfLink(
`https://github.com/${profile.github}`,
`github.com/${profile.github}`,
)}`,
` ${key('linkedin')}${sfLink(
`https://linkedin.com/in/${profile.linkedin}`,
`linkedin.com/in/${profile.linkedin}`,
)}`,
profile.phone ? ` ${key('phone')}${profile.phone}` : '',
].filter(Boolean);
const eduBlock = [
` ${edu.school}`,
...(edu.degrees ?? []).map((d: string) => ` ${d}`),
` GPA ${edu.gpa} · Graduation ${edu.graduation}`,
];
const expBlock = experience.map(
(e) => ` ${e.role} @ ${e.company} (${e.dates})`,
);
const projBlock = projects.slice(0, 4).map((p) => ` ${p.name}`);
const skillBlock = Object.entries(skills).map(
([cat, items]) => ` ${key(catLabel(cat))}${items.join(', ')}`,
);
const system = [
` ${key('age')}<span data-sum-age>—</span>`,
profile.citizenship ? ` ${key('citizen')}${profile.citizenship}` : '',
` ${key('datetime')}<span data-sum-dt>—</span>`,
].filter(Boolean);
return `<span class="sumfetch"><span class="sf-name">${profile.name}</span>
${profile.bio}
${title('contact')}
${contact.join('\n')}
${title('education')}
${eduBlock.join('\n')}
${title('experience')}
${expBlock.join('\n')}
${title('projects')}
${projBlock.join('\n')}
${title('skills')}
${skillBlock.join('\n')}
${title('system')}
${system.join('\n')}
type '${sfLink(
config.resume_url,
'resume',
)}' or 'help' · 'ls' to browse sections</span>`;
};
// --- summary: full-text profile (everything, flat) ---
export const summary = async (_args: string[]): Promise<string> => {
const b = (s: string) => `${s}`;
const blocks: string[] = [];
blocks.push(`${profile.name}\n${profile.bio}`);
blocks.push(
[
title('EDUCATION'),
...education.map((e) =>
[
`${e.school}`,
...e.degrees.map((d) => ` ${d}`),
` GPA ${e.gpa} · Graduation ${e.graduation}`,
].join('\n'),
),
].join('\n'),
);
blocks.push(
[
title('EXPERIENCE'),
...experience.map(
(e) =>
`${e.role} @ ${e.company} (${e.dates})\n${e.bullets
.map(b)
.join('\n')}`,
),
].join('\n\n'),
);
blocks.push(
[
title('PROJECTS'),
...projects.map(
(p) => `${p.name} [${p.timeline}]\n${p.bullets.map(b).join('\n')}`,
),
].join('\n\n'),
);
blocks.push(
[
title('ORGANIZATIONS'),
...organizations.map(
(o) =>
`${o.role} @ ${o.org} (${o.dates})\n${o.bullets.map(b).join('\n')}`,
),
].join('\n\n'),
);
blocks.push(
[
title('PUBLICATIONS'),
...publications.map((p) => `${p.title}\n ${p.status}`),
].join('\n\n'),
);
blocks.push(
[
title('SKILLS'),
...Object.entries(skills).map(
([cat, items]) =>
`${(catLabel(cat) + ':').padEnd(13)}${items.join(', ')}`,
),
].join('\n'),
);
return blocks.join('\n\n');
};
export default sumfetch;
export { sumfetch };

@ -0,0 +1,65 @@
// Single accessor for résumé data. resume.json is a generated mirror of
// resume.org (see docs/resume-pipeline.org); nothing else should import the
// JSON directly, so identity/content has exactly one in-code home.
import resume from '../../resume.json';
export type Profile = {
name: string;
github: string;
linkedin: string;
email: string;
phone?: string;
citizenship?: string;
bio: string;
};
export type Experience = {
id?: string;
role: string;
company: string;
dates: string;
bullets: string[];
};
export type Project = {
id?: string;
name: string;
timeline: string;
bullets: string[];
};
export type Education = {
id?: string;
school: string;
gpa: string;
graduation: string;
degrees: string[];
honors: string[];
coursework: string[];
};
export type Organization = {
id?: string;
role: string;
org: string;
dates: string;
bullets: string[];
};
export type Publication = {
id?: string;
title: string;
status: string;
bullets: string[];
};
export type Skills = Record<string, string[]>;
export const profile = resume.profile as Profile;
export const experience = resume.experience as Experience[];
export const projects = resume.projects as Project[];
export const education = resume.education as Education[];
export const organizations = resume.organizations as Organization[];
export const publications = resume.publications as Publication[];
export const skills = resume.skills as Skills;
// Display label for a skills category key: acronyms (<=2 chars) upper-cased
// whole ("ml" -> "ML"), everything else capitalized ("languages" -> "Languages").
export const catLabel = (c: string) =>
c.length <= 2 ? c.toUpperCase() : c[0].toUpperCase() + c.slice(1);
export default resume;

@ -1,4 +1,10 @@
const { colors } = require('./config.json');
const config = require('./config.json');
const themes = require('./themes.json');
// Resolve colors from a named theme in themes.json when specified,
// otherwise fall back to inline colors in config.json
const colors =
(config.theme && themes[config.theme]) ? themes[config.theme] : config.colors;
module.exports = {
content: [

@ -159,4 +159,25 @@
"red": "#EF6155"
}
}
,
"matrix_crt": {
"light": {
"background": "#EAFBEA",
"foreground": "#103410",
"yellow": "#6B8E23",
"green": "#0A8F3B",
"gray": "#3A5F3A",
"blue": "#0E8D7A",
"red": "#B23A3A"
},
"dark": {
"background": "#0A0F0A",
"foreground": "#D2FFD2",
"yellow": "#9ACD32",
"green": "#00FF88",
"gray": "#2E3D2E",
"blue": "#00D4AA",
"red": "#FF3B3B"
}
}
}

Loading…
Cancel
Save