From a8ec735c77594c19e835c443f4b46a900746bdd0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:06:42 +0000 Subject: [PATCH] Port canonical app to MiTerm + de-bloat terminal (redesign phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Nt6ycjAMdUMqGTGtbKocrs --- config.json | 25 +- public/resume.html | 776 ++++++++++++++++++++++++++++ scripts/org-to-json.mjs | 9 +- src/components/History.tsx | 49 ++ src/components/Ps1.tsx | 15 +- src/components/history/History.tsx | 31 -- src/components/history/hook.ts | 27 - src/components/history/interface.ts | 6 - src/components/input.tsx | 21 +- src/pages/_app.tsx | 53 +- src/pages/index.tsx | 21 +- src/styles/global.css | 100 +++- src/utils/api.ts | 3 +- src/utils/bin/api_commands.ts | 36 -- src/utils/bin/commands.ts | 304 +++++++---- src/utils/bin/index.ts | 3 +- src/utils/bin/sumfetch.ts | 190 +++++-- src/utils/resume.ts | 65 +++ tailwind.config.js | 8 +- themes.json | 21 + 20 files changed, 1460 insertions(+), 303 deletions(-) create mode 100644 public/resume.html create mode 100644 src/components/History.tsx delete mode 100644 src/components/history/History.tsx delete mode 100644 src/components/history/hook.ts delete mode 100644 src/components/history/interface.ts delete mode 100644 src/utils/bin/api_commands.ts create mode 100644 src/utils/resume.ts diff --git a/config.json b/config.json index d1fa47c..e6a2179 100644 --- a/config.json +++ b/config.json @@ -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", diff --git a/public/resume.html b/public/resume.html new file mode 100644 index 0000000..48ed10e --- /dev/null +++ b/public/resume.html @@ -0,0 +1,776 @@ + + + + + + Mihir Talati – Resume + + + +
+
+

Mihir Talati

+

Dual Degree · B.S. Computer Science (Honors) & B.S. Physics (Honors) · GPA 3.974

+
+ +
+

Education

+
+
+
+
+

University of Maryland – College Park

+

Computer Science (Honors) · Physics (Honors)

+
+
+ Expected Graduation · May 2027 + Dean’s List · Every Semester +
+
+
+

Scholarships & Distinctions

+
    +
  • Angelo Bardasis Scholarship (Physics)
  • +
  • John D. Gannon Scholarship (Computer Science)
  • +
+
+
+
+
+ +
+

Work Experience

+ +
+
+
+

Researcher · CAAR REU

+

Combinatorics, Algorithms, and AI for Real Problems

+
+
+ Jun 2025 – Aug 2025 + QuICS · University of Maryland +
+
+
    +
  • Python
  • +
  • Galois
  • +
  • NumPy
  • +
  • Magma Math
  • +
  • SageMath
  • +
+
    +
  • Designed novel fault-tolerant CSS quantum error-correcting codes for qudits inspired by quadratic residue codes.
  • +
  • Established code families with transversal Clifford and T gate support through finite field and QEC theory proofs.
  • +
  • Implemented parallel Python tooling to validate self-orthogonality and distance properties across large search spaces.
  • +
+
+ +
+
+
+

Undergraduate Researcher · Bright Beams Collective

+

IREAP Research Experience for Undergraduates

+
+
+ Sep 2023 – Dec 2024 + Institute for Research in Electronics and Applied Physics +
+
+
    +
  • MAD-X
  • +
  • Elegant
  • +
  • Fusion 360
  • +
  • MATLAB
  • +
  • Ansys Workbench
  • +
+
    +
  • Built custom machine learning and gradient-based optimizers to derive design parameters for a planned beamline.
  • +
  • Ran accelerator simulations to analyze lattice stability and beam quality under varied operating configurations.
  • +
  • Produced animations of betatron motion for outreach presentations and technical discussions.
  • +
+
+ +
+
+
+

Software Engineering Intern · JODO PAY

+

Invoicing System Automation

+
+
+ Jun 2022 – Jul 2022 +
+
+
    +
  • JavaScript
  • +
  • TypeScript
  • +
  • Google Apps Script
  • +
+
    +
  • Automated invoice generation and dispatch for clients, reducing manual processing by 40 staff hours each week.
  • +
  • Integrated Google Workspace APIs to manage templates, customer data, and error handling workflows.
  • +
+
+
+ +
+

Projects

+ +
+
+
+

Quantum State Tomography via QML

+

GitHub · github.com/Mihir-Null

+
+
+ Sep 2024 – Jul 2025 +
+
+
    +
  • Python
  • +
  • Qiskit
  • +
  • Aer
  • +
  • MPI
  • +
  • Numba
  • +
  • PyTorch
  • +
+
    +
  • Co-developed a variational circuit tomography approach, exploring loss landscapes, embedding strategies, and optimizers.
  • +
  • Executed 2,000+ experiments across multi-GPU clusters with a custom orchestrator for reproducible benchmarking.
  • +
  • Co-authored Quantum state reconstruction with variational quantum circuit (arXiv:2507.01246).
  • +
+
+ +
+
+
+

MIT iQuHack · QuEra Challenge

+

Honorable Mention

+
+
+ Feb 2025 +
+
+
    +
  • Python
  • +
  • Qiskit
  • +
  • Cirq
  • +
  • Bloqade
  • +
  • PyTorch
  • +
  • TensorFlow Quantum
  • +
+
    +
  • Designed a compilation pipeline mapping diverse quantum circuits—including QAOA and Steane codes—to QuEra hardware.
  • +
  • Optimized pulse sequences for neutral atom instruction sets, contributing to the team’s honorable mention finish.
  • +
+
+ +
+
+
+

Quantum Machine Learning Research

+

Undergraduate Research Portfolio

+
+
+ Aug 2023 – Dec 2024 +
+
+
    +
  • Python
  • +
  • Qiskit
  • +
  • Cirq
  • +
  • PennyLane
  • +
  • Q#
  • +
+
    +
  • Implemented quantum image processing with parameterized circuit kernels integrated into CNN architectures.
  • +
  • Created a hybrid transitional optimizer to mitigate barren plateaus in QAOA, presented at the FYIRE Summit 2024.
  • +
+
+ +
+
+
+

Terrapin Rocket Team · Kalman Filters

+

Guidance, Navigation, and Control

+
+
+ Aug 2023 – Jan 2024 +
+
+
    +
  • C
  • +
  • C++
  • +
  • MATLAB
  • +
+
    +
  • Developed six- and nine-state Kalman filters fusing multi-sensor telemetry for rocket position estimation.
  • +
  • Supported the team’s winning entry at the 2024 Spaceport America Cup.
  • +
+
+ +
+
+
+

Robotics @ UMD · Computer Vision

+

Testudog Automation

+
+
+ Aug 2023 – Jan 2024 +
+
+
    +
  • Python
  • +
  • OpenCV
  • +
  • scikit-learn
  • +
+
    +
  • Developed Haar cascade pipelines for the Testudog robotics platform deployed on a Jetson-powered Boston Dynamics Spot.
  • +
+
+ +
+
+
+

Club Administration Automation

+

Workflow Automation Suite

+
+
+ Nov 2024 – Feb 2025 +
+
+
    +
  • Python
  • +
  • Selenium
  • +
  • Discord.py
  • +
  • REST APIs
  • +
  • OAuth 2.0
  • +
+
    +
  • Automated Discord announcements, email campaigns, Instagram publishing, and room reservations for university clubs.
  • +
  • Currently deployed across multiple organizations to streamline administrative overhead.
  • +
+
+ +
+
+
+

Housing Market Analysis

+

Predictive Pricing Models

+
+
+ Jun 2024 – Jul 2024 +
+
+
    +
  • Python
  • +
  • Pandas
  • +
  • SciPy
  • +
  • NumPy
  • +
  • scikit-learn
  • +
  • SQL
  • +
+
    +
  • Trained feedforward neural networks for price prediction using engineered features derived from public listings and census data.
  • +
  • Applied PCA and statistical analysis to identify high-signal attributes and reduce overfitting.
  • +
+
+ +
+
+
+

Independent Game Development

+

National Game Jam Winner

+
+
+ 2020 – 2023 +
+
+
    +
  • C#
  • +
  • C++
  • +
  • Unity
  • +
  • Unreal Engine
  • +
  • Blender
  • +
  • Adobe Suite
  • +
+
    +
  • Published two action platformers, including a custom 2D soft-body simulation available via Two Guys Studios on itch.io.
  • +
  • Placed second in the 2021 Mindbox National Design Championship; finalist in 2020 edition.
  • +
  • Built a procedural map generation toolkit leveraging noise algorithms and custom pathfinding.
  • +
+
+ +
+
+
+

Author · A Very Quick Guide to Special Relativity

+

Educational Publication

+
+
+ Sep 2020 – Jun 2021 +
+
+
    +
  • Authored and published an accessible introduction to special relativity covering time dilation through mass–energy equivalence.
  • +
+
+
+ +
+

Organizations

+ +
+
+
+

Industry Chair · Quantum Coalition

+
+
+ Jan 2024 – Present +
+
+
    +
  • Co-organized the QRISE research competition with 200+ international participants.
  • +
  • Partnered with the UN and ITU to deliver the QC-FLIQ hackathon for the International Year of Quantum (500+ attendees).
  • +
+
+ +
+
+
+

President · Undergraduate Quantum Association

+
+
+ Jan 2024 – Present +
+
+
    +
  • Led organizational strategy, programming, and outreach after serving as officer since 2020.
  • +
  • Produced the Quantum Leap Career Nexus with 240+ attendees and partners including IBM, Microsoft, and IonQ.
  • +
+
+ +
+
+
+

Co-President · ACM@UMD

+
+
+ Sep 2023 – Present +
+
+
    +
  • Restarted the university chapter, launched a new website, and grew membership beyond 145 students.
  • +
  • Led workshops focused on shell scripting, AI, quantum computing, and heuristic search techniques.
  • +
+
+
+ +
+

Relevant Courses

+
+ + +
+ Graduate Quantum Computing (CMSC657) + Computer Science Honors Seminar + Differentiable Programming for Agentic AI (CMSC498Z) + Natural Language Processing (CMSC470) + Data Science (CMSC320) + Design & Analysis of Algorithms (CMSC451) + Object-Oriented Programming in Java (CMSC132) + Applied Probability & Statistics (STAT400) + Honors Advanced Modern Physics (PHYS400) + Quantum Mechanics II (PHYS402) + Multivariate Calculus + Differential Equations +
+
No courses match that search. Try a broader keyword.
+
+
+
+ + + + diff --git a/scripts/org-to-json.mjs b/scripts/org-to-json.mjs index 547fe10..b97010c 100644 --- a/scripts/org-to-json.mjs +++ b/scripts/org-to-json.mjs @@ -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; } diff --git a/src/components/History.tsx b/src/components/History.tsx new file mode 100644 index 0000000..7fb00b8 --- /dev/null +++ b/src/components/History.tsx @@ -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(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) => ( +
+
+
+ +
+
{entry.command}
+
+

+

+ ))} + +); diff --git a/src/components/Ps1.tsx b/src/components/Ps1.tsx index 3db90ee..99969c2 100644 --- a/src/components/Ps1.tsx +++ b/src/components/Ps1.tsx @@ -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 (
- - {config.ps1_username} - - @ - - {config.ps1_hostname} - - :$ ~ + {name} + @ + {config.ps1_hostname} + :$ ~
); }; diff --git a/src/components/history/History.tsx b/src/components/history/History.tsx deleted file mode 100644 index e67513a..0000000 --- a/src/components/history/History.tsx +++ /dev/null @@ -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 }> = ({ - history, -}) => { - return ( - <> - {history.map((entry: HistoryInterface, index: number) => ( -
-
-
- -
- -
{entry.command}
-
- -

-

- ))} - - ); -}; - -export default History; diff --git a/src/components/history/hook.ts b/src/components/history/hook.ts deleted file mode 100644 index d453033..0000000 --- a/src/components/history/hook.ts +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { History } from './interface'; - -export const useHistory = (defaultValue: Array) => { - const [history, setHistory] = React.useState>(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([]), - }; -}; diff --git a/src/components/history/interface.ts b/src/components/history/interface.ts deleted file mode 100644 index 1d8dafd..0000000 --- a/src/components/history/interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface History { - id: number; - date: Date; - command: string; - output: string; -} diff --git a/src/components/input.tsx b/src/components/input.tsx index bf9de68..8afee19 100644 --- a/src/components/input.tsx +++ b/src/components/input.tsx @@ -14,9 +14,21 @@ export const Input = ({ setHistory, setLastCommandIndex, clearHistory, + username, +}: { + inputRef: React.MutableRefObject; + containerRef: React.MutableRefObject; + 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) => { - const commands: [string] = history + const commands: string[] = history .map(({ command }) => command) .filter((command: string) => command); @@ -79,21 +91,22 @@ export const Input = ({ }; return ( -
+
{ const inputRef = React.useRef(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 ( <> + {config.title} +
; @@ -22,7 +21,9 @@ const IndexPage: React.FC = ({ 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 = ({ inputRef }) => { {config.title} -
+
- = ({ inputRef }) => { />
+ + + resume ↗ + ); }; diff --git a/src/styles/global.css b/src/styles/global.css index 6f42924..fecb478 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -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; } diff --git a/src/utils/api.ts b/src/utils/api.ts index 6840cac..2d4f3b7 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -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; }; diff --git a/src/utils/bin/api_commands.ts b/src/utils/bin/api_commands.ts deleted file mode 100644 index ce4d487..0000000 --- a/src/utils/bin/api_commands.ts +++ /dev/null @@ -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 => { - const projects = await getProjects(); - return projects - .map( - (repo) => - `${repo.name} - ${repo.html_url}`, - ) - .join('\n'); -}; - -export const quote = async (args: string[]): Promise => { - const data = await getQuote(); - return data.quote; -}; - -export const readme = async (args: string[]): Promise => { - const readme = await getReadme(); - return `Opening GitHub README...\n - ${readme}`; -}; - -export const weather = async (args: string[]): Promise => { - const city = args.join('+'); - if (!city) { - return 'Usage: weather [city]. Example: weather casablanca'; - } - const weather = await getWeather(city); - return weather; -}; diff --git a/src/utils/bin/commands.ts b/src/utils/bin/commands.ts index 338c9f8..714f7ea 100644 --- a/src/utils/bin/commands.ts +++ b/src/utils/bin/commands.ts @@ -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) => + `${text}`; + +// 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 => { + 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 => { + 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 => { - 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 => { + const section = (args[0] ?? '').toLowerCase().replace(/\/$/, ''); + + if (!section) { + const rows = SECTIONS.map(([name, desc], i) => { + const branch = i === SECTIONS.length - 1 ? '└' : '├'; + return `${branch} ${name.padEnd( + 15, + )}${desc}`; + }).join('\n'); + return `~/\n${rows}\n\nuse 'ls <section>' 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 => { - window.open(`${config.repo}`); - return 'Opening Github repository...'; -}; -// About -export const about = async (args: string[]): Promise => { - 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 => { - window.open(`${config.resume_url}`); - return 'Opening resume...'; +export const resume = async (_args: string[]): Promise => { + window.open(config.resume_url); + return 'Opening résumé...'; }; -// Donate -export const donate = async (args: string[]): Promise => { - return `thank you for your interest. -here are the ways you can support my work: -- paypal -- patreon -`; +export const email = async (_args: string[]): Promise => { + window.open(`mailto:${profile.email}`); + return `Opening mailto:${profile.email}...`; }; -// Contact -export const email = async (args: string[]): Promise => { - window.open(`mailto:${config.email}`); - return `Opening mailto:${config.email}...`; +export const github = async (_args: string[]): Promise => { + window.open(`https://github.com/${profile.github}/`); + return 'Opening GitHub...'; }; -export const github = async (args: string[]): Promise => { - window.open(`https://github.com/${config.social.github}/`); - - return 'Opening github...'; -}; - -export const linkedin = async (args: string[]): Promise => { - window.open(`https://www.linkedin.com/in/${config.social.linkedin}/`); - - return 'Opening linkedin...'; +export const linkedin = async (_args: string[]): Promise => { + window.open(`https://www.linkedin.com/in/${profile.linkedin}/`); + return 'Opening LinkedIn...'; }; -// Search export const google = async (args: string[]): Promise => { 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 => { return `Searching duckduckgo for ${args.join(' ')}...`; }; -export const bing = async (args: string[]): Promise => { - 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 => { 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 => { - return args.join(' '); -}; - -export const whoami = async (args: string[]): Promise => { - return `${config.ps1_username}`; -}; +export const echo = async (args: string[]): Promise => args.join(' '); -export const ls = async (args: string[]): Promise => { - return `a -bunch -of -fake -directories`; -}; +export const whoami = async (_args: string[]): Promise => + config.ps1_username; -export const cd = async (args: string[]): Promise => { - return `unfortunately, i cannot afford more directories. -if you want to help, you can type 'donate'.`; -}; +export const date = async (_args: string[]): Promise => + new Date().toString(); -export const date = async (args: string[]): Promise => { - return new Date().toString(); -}; +export const vi = async (_args: string[]): Promise => + `woah, you still use 'vi'? just try 'vim'.`; +export const vim = async (_args: string[]): Promise => + `'vim' is so outdated. how about 'nvim'?`; +export const nvim = async (_args: string[]): Promise => + `'nvim'? too fancy. why not 'emacs'?`; +export const emacs = async (_args?: string[]): Promise => + `you know what? just use vscode.`; -export const vi = async (args: string[]): Promise => { - return `woah, you still use 'vi'? just try 'vim'.`; +export const sudo = async (_args?: string[]): Promise => { + 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 => { - return `'vim' is so outdated. how about 'nvim'?`; -}; +// --- API commands --- -export const nvim = async (args: string[]): Promise => { - return `'nvim'? too fancy. why not 'emacs'?`; +export const github_projects = async (_args: string[]): Promise => { + 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 => { - return `you know what? just use vscode.`; +export const quote = async (_args: string[]): Promise => { + const data = await getQuote(); + return data.quote; }; -export const sudo = async (args?: string[]): Promise => { - 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 => { + 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 here for the Github repository. -`; +export const weather = async (args: string[]): Promise => { + const city = args.join('+'); + if (!city) return 'Usage: weather [city]. Example: weather london'; + return getWeather(city); }; diff --git a/src/utils/bin/index.ts b/src/utils/bin/index.ts index 0d55ba1..9cf48c0 100644 --- a/src/utils/bin/index.ts +++ b/src/utils/bin/index.ts @@ -1,3 +1,2 @@ export * from './commands'; -export * from './api_commands'; -export { default as sumfetch } from './sumfetch'; +export * from './sumfetch'; diff --git a/src/utils/bin/sumfetch.ts b/src/utils/bin/sumfetch.ts index e55686e..2a99372 100644 --- a/src/utils/bin/sumfetch.ts +++ b/src/utils/bin/sumfetch.ts @@ -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 => { - if (config.ascii === 'cveinnt') { - return ` - @@@@@@@@@@@@@  sumfetch: summary display - @@@@ @@@@ ----------- - @@ @@  ABOUT - @@ @@  ${config.name} - @@ @@ ﰩ ${config.ps1_hostname} - @@ @@@ @@  resume -@@ @@@ @@ 爵 Github repo -@@ @@ ----------- -@@ .@@@@@@@@@@. @@  CONTACT - @@ @@ @@ @@  ${config.email} - @@ @@ @@ @@  github.com/${config.social.github} - @@ @@@@@@ @@  linkedin.com/in/${config.social.linkedin} - @@@ @@@ ----------- - @@@ @@@ @@  DONATE - @| @@@@@@@@@@@@@@@@ @@  ${config.donate_urls.paypal} - @| @@  ${config.donate_urls.patreon} - -`; - } else { - return ` - ▄▓▓▓▓▓▓▓▓▓▓▓▓▓▓▄   sumfetch - ▄▓▓▀ ▄▓▓▀▓▓▓▀▓▓▄ ▀▀▓▓▄  ----------- - ▓▓▀ ▄▓▀  ▐▓▓ ▀▓▓  ▓▓▄   ABOUT - ▄▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ${config.name} - ▓▓ ▓▓▓  ▐▓▓  ▐▓▓  ▓▓   resume -▐▓▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▓ 爵 Github repo -▐▓  ▐▓ ----------- -▐▓  > L I V E T E R M ▐▓  CONTACT -▐▓  ▐▓  ${config.email} -▐▓▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▓  github.com/${config.social.github} - ▓▓ ▐▓▓  ▓▓  ▐▓▓  ▓▓   linkedin.com/in/${config.social.linkedin} - ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  ----------- - ▓▓▓  ▐▓▓  ▓▓  ▓▓▓ ▓▓▀   DONATE - ▀▓▓▄▄ ▀▓▓▄▓▓▄▓▓▓▄▄▓▓▀   ${config.donate_urls.paypal} - ▀▓▓▓▓▓▓▓▓▓▓▓▀▀   ${config.donate_urls.patreon} - -`; - } +const key = (k: string, w = 11) => `${k.padEnd(w)}`; +const title = (t: string) => `${t}`; +const sfLink = (href: string, text: string) => + `${text}`; + +// --- sumfetch: clean flat profile card (the landing screen) --- +const sumfetch = async (_args: string[]): Promise => { + 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')}`, + profile.citizenship ? ` ${key('citizen')}${profile.citizenship}` : '', + ` ${key('datetime')}`, + ].filter(Boolean); + + return `${profile.name} + ${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`; +}; + +// --- summary: full-text profile (everything, flat) --- +export const summary = async (_args: string[]): Promise => { + 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 }; diff --git a/src/utils/resume.ts b/src/utils/resume.ts new file mode 100644 index 0000000..c43b351 --- /dev/null +++ b/src/utils/resume.ts @@ -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; + +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; diff --git a/tailwind.config.js b/tailwind.config.js index 2c63b9b..1025852 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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: [ diff --git a/themes.json b/themes.json index da2b297..e3e58d2 100644 --- a/themes.json +++ b/themes.json @@ -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" + } + } }