Skip to content

Commit 9d0dd88

Browse files
Add various terminal scripts and components
- Implement TextProgram for rendering text in the terminal. - Create ASCII art text renderer in ascii_image.py. - Develop a safe scientific calculator in calc.py. - Introduce a minimal chess game in chess.py. - Add a dice roller with statistics in dice_roll.py. - Create a Hello World demo with runtime info in hello_world.py. - Implement a media demo helper in media_demo.py. - Develop a minimal Pacman game in pacman.py. - Add a turn-based Pong demo in pong.py. - Create a quote bot that provides random quotes in quote_bot.py. - Implement a turn-based Snake game in snake.py. - Develop a minimal Solitaire demo in solitaire.py. - Create an animated ASCII starfield in starfield.py. - Add content validation script in validate-content.ts. - Define terminal types and interfaces in types.ts.
1 parent 10c0f9a commit 9d0dd88

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3858
-127
lines changed

src/app/globals.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3486,3 +3486,24 @@ textarea {
34863486
opacity: calc(1 - var(--content-reveal));
34873487
transition: height 0.3s ease, opacity 0.3s ease;
34883488
}
3489+
3490+
/* Utility - Agent Added */
3491+
.absolute-fill {
3492+
position: absolute;
3493+
top: 0;
3494+
left: 0;
3495+
width: 100%;
3496+
height: 100%;
3497+
}
3498+
3499+
.terminal-hidden-layer {
3500+
position: absolute;
3501+
opacity: 0;
3502+
pointer-events: none;
3503+
}
3504+
3505+
3506+
.hidden {
3507+
display: none;
3508+
}
3509+

src/components/HomeClient.tsx

Lines changed: 84 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ import {
1111
useState,
1212
} from "react";
1313
import { motion, useReducedMotion } from "framer-motion";
14+
import { CanvasTexture } from "three";
1415
import ContactForm from "@/components/ContactForm";
1516
import Markdown from "@/components/Markdown";
16-
import TerminalCanvas, {
17-
TerminalApi,
18-
} from "@/components/terminal/TerminalCanvas";
19-
import type { SceneDebugInfo } from "@/components/hero/HeroScene";
17+
import { Terminal, type TerminalRef } from "@/components/terminal/Terminal";
2018
import { usePreferences } from "@/components/PreferencesProvider";
2119
import type { ContentData } from "@/lib/content";
2220
import { getAlternateLanguage, languageMeta } from "@/lib/i18n";
@@ -27,10 +25,13 @@ function HeroLoading() {
2725
return <div className="hero-loading">{t.hero.loading}</div>;
2826
}
2927

30-
const HeroScene = dynamic(() => import("@/components/hero/HeroScene"), {
31-
ssr: false,
32-
loading: () => <HeroLoading />,
33-
});
28+
const Scene = dynamic(
29+
() => import("@/components/canvas/Scene").then((mod) => mod.Scene),
30+
{
31+
ssr: false,
32+
loading: () => <HeroLoading />,
33+
},
34+
);
3435

3536
type HomeClientProps = {
3637
content: ContentData;
@@ -497,6 +498,17 @@ export default function HomeClient({
497498
const { profile, projects, theme, pages, capabilities } = content;
498499
const { t, language, setLanguage } = useI18n();
499500
const { theme: currentTheme, toggleTheme, isSwitching } = usePreferences();
501+
const terminalTheme = useMemo(
502+
() => ({
503+
palette: {
504+
terminalBg: theme.palette.terminalBg,
505+
terminalText: theme.palette.terminalText,
506+
terminalDim: theme.palette.terminalDim,
507+
},
508+
crt: theme.crt,
509+
}),
510+
[theme],
511+
);
500512
const heroRef = useRef<HTMLElement>(null);
501513
const featuredRef = useRef<HTMLElement>(null);
502514
const featuredHeadingRef = useRef<HTMLHeadingElement>(null);
@@ -641,7 +653,9 @@ export default function HomeClient({
641653
const [openProjectId, setOpenProjectId] = useState<string | null>(null);
642654
const [openCapabilityId, setOpenCapabilityId] = useState<string | null>(null);
643655
const [aboutOpen, setAboutOpen] = useState(false);
644-
const [terminalApi, setTerminalApi] = useState<TerminalApi | null>(null);
656+
const [terminalTexture, setTerminalTexture] = useState<CanvasTexture | null>(
657+
null,
658+
);
645659
const [terminalFocused, setTerminalFocused] = useState(false);
646660
const pendingTerminalFocusRef = useRef(false);
647661
const [screenAspect, setScreenAspect] = useState(1.33);
@@ -668,30 +682,33 @@ export default function HomeClient({
668682
alt: false,
669683
});
670684
const mobileModifiersRef = useRef(mobileModifiers);
685+
const terminalRef = useRef<TerminalRef>(null);
686+
const terminalUser = useMemo(
687+
() =>
688+
profile.terminal.prompt.split("@")[0] || t.terminal.system.userFallback,
689+
[profile.terminal.prompt, t.terminal.system.userFallback],
690+
);
691+
const homeDir = useMemo(() => `/home/${terminalUser}`, [terminalUser]);
692+
671693
const handleSceneReady = useCallback(() => {
672694
setSceneReady(true);
673695
setSceneReadyAt(Date.now());
674-
}, []);
675-
676-
const handleSceneDebug = useCallback((info: SceneDebugInfo) => {
677-
if (info.modelLoaded || info.fallbackPlane) {
678-
setModelReady(true);
679-
setModelReadyAt(Date.now());
680-
}
696+
setModelReady(true);
697+
setModelReadyAt(Date.now());
681698
}, []);
682699

683700
const handleTerminalFocus = useCallback(() => {
684-
if (terminalApi) {
685-
terminalApi.focus();
701+
if (terminalRef.current) {
702+
terminalRef.current.focusInput();
686703
pendingTerminalFocusRef.current = false;
687704
return;
688705
}
689706
pendingTerminalFocusRef.current = true;
690-
}, [terminalApi]);
707+
}, []);
691708

692709
const handleTerminalInputFocus = useCallback(() => {
693-
terminalApi?.focusInput?.();
694-
}, [terminalApi]);
710+
terminalRef.current?.focusInput();
711+
}, []);
695712

696713
const handleVirtualKey = useCallback(
697714
(
@@ -701,27 +718,18 @@ export default function HomeClient({
701718
if (options?.focus !== false) {
702719
handleTerminalFocus();
703720
}
704-
const modifiers = mobileModifiersRef.current;
705-
const down = new KeyboardEvent("keydown", {
706-
key,
707-
bubbles: true,
708-
ctrlKey: modifiers.ctrl,
709-
shiftKey: modifiers.shift,
710-
altKey: modifiers.alt,
711-
});
712-
const up = new KeyboardEvent("keyup", {
713-
key,
714-
bubbles: true,
715-
ctrlKey: modifiers.ctrl,
716-
shiftKey: modifiers.shift,
717-
altKey: modifiers.alt,
718-
});
719-
window.dispatchEvent(down);
720-
window.dispatchEvent(up);
721+
terminalRef.current?.handleInput(key);
721722
},
722723
[handleTerminalFocus],
723724
);
724725

726+
useEffect(() => {
727+
if (terminalTexture && !terminalBootReady) {
728+
setTerminalBootReady(true);
729+
setTerminalBootReadyAt(Date.now());
730+
}
731+
}, [terminalTexture, terminalBootReady]);
732+
725733
const toggleMobileModifier = useCallback((key: "ctrl" | "shift" | "alt") => {
726734
setMobileModifiers((prev) => {
727735
const next = { ...prev, [key]: !prev[key] };
@@ -1091,15 +1099,15 @@ export default function HomeClient({
10911099
}, [bootDone]);
10921100

10931101
useEffect(() => {
1094-
if (!terminalApi || !pendingTerminalFocusRef.current) {
1102+
if (!terminalRef.current || !pendingTerminalFocusRef.current) {
10951103
return;
10961104
}
1097-
terminalApi.focus();
1105+
terminalRef.current.focusInput();
10981106
pendingTerminalFocusRef.current = false;
1099-
}, [terminalApi]);
1107+
}, []);
11001108

11011109
useEffect(() => {
1102-
if (!bootDone || !terminalApi || isMobile || isCoarsePointer) {
1110+
if (!bootDone || !terminalRef.current || isMobile || isCoarsePointer) {
11031111
return;
11041112
}
11051113
const active = document.activeElement;
@@ -1110,8 +1118,8 @@ export default function HomeClient({
11101118
) {
11111119
return;
11121120
}
1113-
terminalApi.focus();
1114-
}, [bootDone, isCoarsePointer, isMobile, terminalApi]);
1121+
terminalRef.current.focusInput();
1122+
}, [bootDone, isCoarsePointer, isMobile]);
11151123

11161124
useEffect(() => {
11171125
mobileModifiersRef.current = mobileModifiers;
@@ -1341,9 +1349,6 @@ export default function HomeClient({
13411349
}, [latestProjects, t.projects.latestTitle]);
13421350

13431351
const files = useMemo(() => {
1344-
const terminalUser =
1345-
profile.terminal.prompt.split("@")[0] || t.terminal.system.userFallback;
1346-
const homeDir = `/home/${terminalUser}`;
13471352
const docsDir = `${homeDir}/docs`;
13481353
const scriptsDir = `${homeDir}/scripts`;
13491354
const mediaDir = `${homeDir}/media`;
@@ -1588,14 +1593,7 @@ export default function HomeClient({
15881593
content: t.terminal.files.elfPlaceholder,
15891594
},
15901595
];
1591-
}, [
1592-
localizedPages,
1593-
profile.fullName,
1594-
profile.terminal.prompt,
1595-
projectsMd,
1596-
t.terminal.files,
1597-
t.terminal.system.userFallback,
1598-
]);
1596+
}, [localizedPages, profile.fullName, projectsMd, t.terminal.files, homeDir]);
15991597

16001598
const scrollToSection = (id: string) => {
16011599
const element = document.getElementById(id);
@@ -1605,6 +1603,26 @@ export default function HomeClient({
16051603
setMenuOpen(false);
16061604
};
16071605

1606+
const terminalConfig = useMemo(
1607+
() => ({
1608+
prompt: profile.terminal.prompt,
1609+
introLines: profile.introLines[language],
1610+
homePath: homeDir,
1611+
files,
1612+
messages: t.terminal,
1613+
onNavigateAction: scrollToSection,
1614+
}),
1615+
[
1616+
files,
1617+
homeDir,
1618+
language,
1619+
profile.introLines,
1620+
profile.terminal.prompt,
1621+
scrollToSection,
1622+
t.terminal,
1623+
],
1624+
);
1625+
16081626
const navLabels: Record<string, string> = {
16091627
home: t.nav.home,
16101628
intro: t.nav.intro,
@@ -2222,45 +2240,10 @@ export default function HomeClient({
22222240
id="home"
22232241
className={`hero-shell ${terminalFocused ? "focused" : ""}`}>
22242242
<div className="hero-sticky">
2225-
<HeroScene
2226-
terminalApi={terminalApi}
2243+
<Scene
2244+
texture={terminalTexture}
22272245
scrollProgressRef={scrollProgressRef}
2228-
noteTexts={{ red: t.hero.noteRed, blue: t.hero.noteBlue }}
2229-
active={heroActive}
2230-
onDebugAction={handleSceneDebug}
2231-
onScreenAspectAction={(aspect) => {
2232-
if (!screenAspectReady) {
2233-
setScreenAspect(aspect);
2234-
setScreenAspectReady(true);
2235-
setScreenAspectReadyAt(Date.now());
2236-
screenAspectRef.current = aspect;
2237-
pendingAspectRef.current = null;
2238-
return;
2239-
}
2240-
const diff = Math.abs(aspect - screenAspectRef.current);
2241-
if (diff < 0.03) {
2242-
return;
2243-
}
2244-
pendingAspectRef.current = aspect;
2245-
if (interactionActiveRef.current) {
2246-
return;
2247-
}
2248-
if (screenAspectTimerRef.current) {
2249-
window.clearTimeout(screenAspectTimerRef.current);
2250-
}
2251-
screenAspectTimerRef.current = window.setTimeout(() => {
2252-
if (pendingAspectRef.current === null) {
2253-
return;
2254-
}
2255-
const nextAspect = pendingAspectRef.current;
2256-
pendingAspectRef.current = null;
2257-
setScreenAspect(nextAspect);
2258-
if (!screenAspectReady) {
2259-
setScreenAspectReady(true);
2260-
setScreenAspectReadyAt(Date.now());
2261-
}
2262-
}, 140);
2263-
}}
2246+
className="hero-scene absolute-fill"
22642247
onReadyAction={handleSceneReady}
22652248
/>
22662249
<div className="hero-overlay">
@@ -2384,26 +2367,14 @@ export default function HomeClient({
23842367
className={`hero-scroll-shield${heroActive ? " is-active" : ""}`}
23852368
aria-hidden="true"
23862369
/>
2387-
<TerminalCanvas
2388-
files={files}
2389-
introLines={profile.introLines[language]}
2390-
prompt={profile.terminal.prompt}
2391-
language={language}
2392-
messages={t.terminal}
2393-
theme={theme}
2394-
isMobile={isMobile}
2395-
isActive={heroActive}
2396-
screenAspect={screenAspect}
2397-
scrollProgressRef={scrollProgressRef}
2398-
onNavigateAction={scrollToSection}
2399-
onReadyAction={(api) => {
2400-
setTerminalApi(api);
2401-
}}
2402-
onBootReadyAction={() => {
2403-
setTerminalBootReady(true);
2404-
setTerminalBootReadyAt(Date.now());
2405-
}}
2406-
onFocusChangeAction={(focused) => setTerminalFocused(focused)}
2370+
<Terminal
2371+
ref={terminalRef}
2372+
config={terminalConfig}
2373+
theme={terminalTheme}
2374+
active={heroActive}
2375+
focused={terminalFocused}
2376+
onFocusChange={setTerminalFocused}
2377+
onTextureReady={setTerminalTexture}
24072378
/>
24082379
</section>
24092380

0 commit comments

Comments
 (0)