fix: terminal promtp

This commit is contained in:
Barrett Ruth 2025-10-08 15:47:17 -04:00
parent cefde24774
commit 4076af3592
6 changed files with 212 additions and 246 deletions

View file

@ -1,126 +1,194 @@
const TERMINAL_PROMPT = "barrett@ruth:~$ ";
let typing = false;
let clearing = false;
(() => {
if (window.__BT_INDEX_INIT) return;
window.__BT_INDEX_INIT = true;
function clearPrompt(delay, callback) {
if (clearing) return;
clearing = true;
const TERMINAL_PROMPT = "barrett@ruth:~$ ";
let typing = false;
let clearing = false;
const terminalPrompt = document.querySelector(".terminal-prompt");
if (!terminalPrompt) {
clearing = false;
return;
function promptEl() {
return document.querySelector(".terminal-prompt");
}
const topicLength =
terminalPrompt.innerHTML.length - TERMINAL_PROMPT.length;
let i = 0;
(function restorePrompt() {
const saved = sessionStorage.getItem("terminalPromptText");
const el = promptEl();
if (saved && el) el.textContent = saved;
sessionStorage.removeItem("terminalPromptText");
})();
function removeChar() {
if (i++ < topicLength) {
terminalPrompt.textContent = terminalPrompt.textContent.slice(0, -1);
setTimeout(removeChar, delay / topicLength);
} else {
i = 0;
function clearPrompt(delay, callback) {
if (clearing) return;
clearing = true;
const el = promptEl();
if (!el) {
clearing = false;
callback && callback();
return;
}
}
removeChar();
}
function typechars(e) {
e.preventDefault();
const topicElement = e.target;
if (topicElement.classList.contains("active")) return;
if (typing) return;
typing = true;
const topic = topicElement.dataset.topic;
const terminalText = ` /${topic.toLowerCase()}`;
const terminalPrompt = document.querySelector(".terminal-prompt");
const delay =
terminalPrompt.innerHTML.length > TERMINAL_PROMPT.length ? 250 : 500;
const topics = document.querySelectorAll(".topic a");
topics.forEach((t) => {
t.classList.remove("active");
t.style.color = "";
});
topicElement.classList.add("active");
topicElement.style.color = getTopicColor(topic);
clearPrompt(delay, () => {
const excess = el.textContent.length - TERMINAL_PROMPT.length;
let i = 0;
function typechar() {
if (i < terminalText.length) {
terminalPrompt.innerHTML += terminalText.charAt(i++);
setTimeout(typechar, delay / terminalText.length);
function tick() {
if (i++ < excess) {
el.textContent = el.textContent.slice(0, -1);
setTimeout(tick, delay / Math.max(excess, 1));
} else {
renderPosts(topic);
typing = false;
clearing = false;
callback && callback();
}
}
typechar();
});
}
function renderPosts(topic) {
const posts = document.getElementById("posts");
posts.innerHTML = "";
const categoryPosts = postsByCategory[topic];
if (!categoryPosts) {
return;
tick();
}
categoryPosts.forEach((post) => {
const postDiv = document.createElement("div");
postDiv.classList.add("post");
const link = document.createElement("a");
const slug = post.id
.split("/")
.pop()
.replace(/\.mdx?$/, "");
link.href = `/posts/${topic}/${slug}.html`;
link.textContent = post.data.title;
link.style.textDecoration = "underline";
postDiv.appendChild(link);
posts.appendChild(postDiv);
});
}
document.addEventListener("DOMContentLoaded", function () {
const topics = document.querySelectorAll(".topic a");
topics.forEach((topic) => {
topic.addEventListener("click", typechars);
const topicName = topic.dataset.topic;
topic.addEventListener("mouseenter", () => {
const color = window.getTopicColor(topicName);
topic.style.color = color;
});
topic.addEventListener("mouseleave", () => {
if (!topic.classList.contains("active")) {
topic.style.color = "";
function typeTerminalPath(topic, delay, callback) {
if (typing) return;
typing = true;
const el = promptEl();
if (!el) return;
const txt = ` /${topic}`;
clearPrompt(delay, () => {
let i = 0;
function step() {
if (i < txt.length) {
el.textContent += txt.charAt(i++);
setTimeout(step, delay / txt.length);
} else {
typing = false;
callback && callback();
}
}
step();
});
}
function persistPrompt() {
const el = promptEl();
if (el) sessionStorage.setItem("terminalPromptText", el.textContent);
}
function renderPosts(topic) {
if (!window.postsByCategory) return;
const posts = document.getElementById("posts");
if (!posts) return;
posts.innerHTML = "";
const arr = window.postsByCategory[topic];
if (!arr) return;
for (const post of arr) {
const div = document.createElement("div");
div.className = "post";
const a = document.createElement("a");
const slug = post.id.split("/").pop().replace(/\.mdx?$/, "");
a.href = `/posts/${topic}/${slug}.html`;
a.textContent = post.data.title;
a.style.textDecoration = "underline";
div.appendChild(a);
posts.appendChild(div);
}
}
function handleDataTopicClick(e) {
const link = e.target.closest("[data-topic]");
if (!link) return;
e.preventDefault();
if (typing) return;
const topic = link.dataset.topic.toLowerCase();
const href = link.getAttribute("href") || "/";
const delay = 500;
const path = window.location.pathname;
const colorFn = window.getTopicColor || (() => "");
const topics = document.querySelectorAll("[data-topic]");
topics.forEach((t) => {
t.classList.remove("active");
t.style.color = "";
});
link.classList.add("active");
const c = colorFn(topic);
if (c) link.style.color = c;
typeTerminalPath(topic, delay, () => {
persistPrompt();
if (path === "/" || path === "/index.html") {
if (window.postsByCategory && window.postsByCategory[topic]) {
renderPosts(topic);
return;
}
}
const isMail = href.startsWith("mailto:");
if (isMail) {
window.location.href = href;
return;
}
if (link.target === "_blank") {
window.open(href, "_blank");
return;
}
window.location.href = href;
});
}
function handleHomeClick(e) {
const home = e.target.closest(".home-link");
if (!home) return;
e.preventDefault();
const isHome =
window.location.pathname === "/" ||
window.location.pathname === "/index.html";
if (isHome) {
clearPrompt(500, () => {
const posts = document.getElementById("posts");
if (posts) posts.innerHTML = "";
const topics = document.querySelectorAll("[data-topic].active");
topics.forEach((t) => {
t.classList.remove("active");
t.style.color = "";
});
});
} else {
persistPrompt();
clearPrompt(500, () => {
window.location.href = "/";
});
}
}
document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener("click", (e) => {
if (e.target.closest(".home-link")) return handleHomeClick(e);
if (e.target.closest("[data-topic]")) return handleDataTopicClick(e);
});
document.body.addEventListener(
"mouseenter",
(e) => {
const link = e.target.closest("[data-topic]");
if (!link) return;
const color =
(window.getTopicColor &&
window.getTopicColor(link.dataset.topic.toLowerCase())) ||
"";
if (color) link.style.color = color;
},
true
);
document.body.addEventListener(
"mouseleave",
(e) => {
const link = e.target.closest("[data-topic]");
if (!link) return;
if (!link.classList.contains("active")) link.style.color = "";
},
true
);
window.addEventListener("beforeunload", () => {
const el = promptEl();
if (el) el.textContent = TERMINAL_PROMPT;
});
});
window.addEventListener("beforeunload", () => {
const terminalPrompt = document.querySelector(".terminal-prompt");
if (terminalPrompt) {
terminalPrompt.innerHTML = TERMINAL_PROMPT;
}
});
});
})();

View file

@ -1,14 +1,10 @@
<footer>
<span>ʕ •ᴥ•ʔ</span>
<div class="footer-links">
<a id="gist-link" target="_blank">gist</a>
<a id="git-link" target="_blank">git</a>
<a
target="_blank"
href="https://www.linkedin.com/in/barrett-ruth/"
>linkedin</a
>
<a target="_blank" href="mailto:br.barrettruth@gmail.com">email</a>
<a href="/gist" data-topic="gist">gist</a>
<a href="/git" data-topic="git">git</a>
<a href="https://www.linkedin.com/in/barrett-ruth/" data-topic="linkedin" target="_blank">linkedin</a>
<a href="mailto:br.barrettruth@gmail.com" data-topic="mail">email</a>
</div>
</footer>
@ -20,23 +16,12 @@
align-items: center;
justify-content: space-between;
}
.footer-links a {
margin-left: 25px;
text-decoration: none;
cursor: pointer;
}
</style>
<script>
const host = window.location.hostname;
const gitLink = document.getElementById("git-link");
const gistLink = document.getElementById("gist-link");
<script type="module" src="/scripts/index.js"></script>
if (host.includes("localhost") || host.includes("127.0.0.1")) {
gistLink.href = "http://localhost:4321/gist.html";
gitLink.href = "http://localhost:4321/git.html";
} else {
gistLink.href = "https://barrettruth.com/gist.html";
gitLink.href = "https://barrettruth.com/git.html";
}
</script>

View file

@ -1,11 +1,11 @@
---
const path = Astro.url.pathname;
const isHome = path === "/" || path === "/index.html";
const is404 = path === "/404.html" || path === "/404";
function getTopic() {
function deriveTopic() {
if (is404) return "/not-found";
if (path.startsWith("/about")) return "/about";
if (path === "/gist" || path.startsWith("/gist/")) return "/gist";
if (path === "/git" || path.startsWith("/git/")) return "/git";
if (path.startsWith("/posts/")) {
const parts = path.replace(/\/+$/, "").split("/");
@ -14,132 +14,35 @@ function getTopic() {
return "";
}
const topic = getTopic();
const topic = deriveTopic();
const promptText = topic ? `barrett@ruth:~$ ${topic}` : "barrett@ruth:~$";
---
<header>
<a href="/" style="text-decoration: none; color: inherit">
<a href="/" class="home-link" style="text-decoration: none; color: inherit">
<div class="terminal-container">
<span class="terminal-prompt">{promptText}</span>
<span class="terminal-cursor"></span>
</div>
</a>
<div class="header-links">
<a target="_blank" href="/resume.pdf">resume</a>
<a target="_blank" href="/transcript.pdf">transcript</a>
<a href="/about.html">about</a>
<a href="/resume.pdf" data-topic="resume" target="_blank">resume</a>
<a href="/transcript.pdf" data-topic="transcript" target="_blank">transcript</a>
<a href="/about" data-topic="about">about</a>
</div>
</header>
<script>
const TERMINAL_PROMPT = "barrett@ruth:~$ ";
let clearing = false;
function clearPrompt(delay, callback) {
if (clearing) return;
clearing = true;
const terminalPrompt = document.querySelector(".terminal-prompt");
if (!terminalPrompt) {
clearing = false;
return;
}
const topicLength =
terminalPrompt.innerHTML.length - TERMINAL_PROMPT.length;
let i = 0;
function removeChar() {
if (i++ < topicLength) {
terminalPrompt.textContent = terminalPrompt.textContent.slice(0, -1);
setTimeout(removeChar, delay / topicLength);
} else {
i = 0;
clearing = false;
callback && callback();
}
}
removeChar();
}
function goHome(e) {
e.preventDefault();
clearPrompt(500, () => (window.location.href = "/"));
}
function goToAbout(e) {
e.preventDefault();
const terminalPrompt = document.querySelector(".terminal-prompt");
const terminalText = " /about";
const delay = 500;
clearPrompt(delay, () => {
let i = 0;
function typechar() {
if (i < terminalText.length) {
terminalPrompt.innerHTML += terminalText.charAt(i++);
setTimeout(typechar, delay / terminalText.length);
} else {
window.location.href = "/about.html";
}
}
typechar();
});
}
document.addEventListener("DOMContentLoaded", () => {
window.TERMINAL_PROMPT = TERMINAL_PROMPT;
window.clearPrompt = clearPrompt;
window.goHome = goHome;
const homeLink = document.querySelector('header a[href="/"]');
if (homeLink) {
const path = window.location.pathname;
const isHome = path === "/" || path === "/index.html";
if (isHome) {
homeLink.addEventListener("click", (e) => {
e.preventDefault();
const topics = document.querySelectorAll(".topic a");
topics.forEach((topic) => {
topic.classList.remove("active");
topic.style.color = "";
});
const posts = document.getElementById("posts");
if (posts) posts.innerHTML = "";
clearPrompt(500);
});
} else {
homeLink.addEventListener("click", goHome);
}
}
const aboutLink = document.querySelector('.header-links a[href="/about.html"]');
if (aboutLink) {
const path = window.location.pathname;
const isHome = path === "/" || path === "/index.html";
if (isHome) {
aboutLink.addEventListener("click", goToAbout);
}
}
});
</script>
<style>
.header-links a {
margin-left: 25px;
text-decoration: none;
cursor: pointer;
}
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: flex-start;
}
.header-links {
align-self: flex-end;
display: flex;
@ -147,10 +50,11 @@ const promptText = topic ? `barrett@ruth:~$ ${topic}` : "barrett@ruth:~$";
text-align: right;
gap: 5px;
}
.header-links a {
margin-left: 0;
}
}
</style>
<script type="module" src="/scripts/index.js"></script>

View file

@ -55,6 +55,7 @@ const topicColor = getTopicColor(category);
</div>
<slot name="scripts" slot="scripts">
<script src="/scripts/index.js" is:inline></script>
<script src="/scripts/centerKatex.js" is:inline></script>
{frontmatter.scripts?.map(script => (
<script src={script} type="module"></script>

View file

@ -10,15 +10,23 @@ gists.sort((a, b) => a.slug.localeCompare(b.slug));
<BaseLayout title={title}>
<slot name="head" slot="head">
<link rel="stylesheet" href="/styles/index.css" />
<script type="module" src="/scripts/index.js"></script>
</slot>
<div class="content">
<ul class="topics">
{gists.map((gist) => (
<li class="topic">
<a href={`/gist/${gist.slug}.html`}>{gist.data.title || gist.slug}</a>
<a
href={`/gist/${gist.slug}.html`}
data-topic="gist"
data-gist={gist.slug}
>
{gist.data.title || gist.slug}
</a>
</li>
))}
</ul>
</div>
</BaseLayout>

View file

@ -7,12 +7,11 @@ const title = "Git Repositories";
<BaseLayout title={title}>
<slot name="head" slot="head">
<link rel="stylesheet" href="/styles/index.css" />
<script type="module" src="/scripts/index.js"></script>
</slot>
<div class="content">
<ul class="topics" id="repo-list">
<li class="topic"></li>
</ul>
<ul class="topics" id="repo-list"></ul>
<div class="posts" id="posts"></div>
</div>
@ -31,16 +30,16 @@ const title = "Git Repositories";
repos.sort((a, b) => a.localeCompare(b));
for (const name of repos) {
const label = name.replace(/\.git$/,"");
const cls = `repo-${label}`;
const label = name.replace(/\.git$/, "");
const li = document.createElement("li");
li.className = `topic ${cls}`;
li.className = `topic repo-${label}`;
const a = document.createElement("a");
a.href = `${window.location.origin}/git/${label}.html`;
a.href = `/git/${label}.html`;
a.textContent = label;
a.setAttribute("data-topic", name);
a.dataset.topic = "git";
a.dataset.repo = label;
li.appendChild(a);
listEl.appendChild(li);
@ -51,3 +50,4 @@ const title = "Git Repositories";
loadRepos();
</script>
</BaseLayout>