From 7dc455c360462e7a52b15b5448e9f15440bc980f Mon Sep 17 00:00:00 2001 From: Microck Date: Tue, 10 Feb 2026 19:15:02 +0000 Subject: [PATCH] Fix terminal loading animation in landing page --- landing/.gitignore | 6 + .../reddit-r-bashonubuntuonwindows.md | 36 ++++ landing/promotion/reddit-r-commandline.md | 32 ++++ landing/promotion/reddit-r-cpp.md | 36 ++++ landing/promotion/reddit-r-powershell.md | 34 ++++ landing/promotion/reddit-strategy.md | 106 ++++++++++++ landing/src/app/globals.css | 40 ----- landing/src/app/layout.tsx | 13 -- landing/src/app/page.tsx | 157 +++++++----------- landing/src/components/ui/shiny-text.tsx | 111 +++++++++---- 10 files changed, 394 insertions(+), 177 deletions(-) create mode 100644 landing/promotion/reddit-r-bashonubuntuonwindows.md create mode 100644 landing/promotion/reddit-r-commandline.md create mode 100644 landing/promotion/reddit-r-cpp.md create mode 100644 landing/promotion/reddit-r-powershell.md create mode 100644 landing/promotion/reddit-strategy.md diff --git a/landing/.gitignore b/landing/.gitignore index 5ef6a52..a79a143 100644 --- a/landing/.gitignore +++ b/landing/.gitignore @@ -39,3 +39,9 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# production build +/dist + +# production build +/dist diff --git a/landing/promotion/reddit-r-bashonubuntuonwindows.md b/landing/promotion/reddit-r-bashonubuntuonwindows.md new file mode 100644 index 0000000..9abad6c --- /dev/null +++ b/landing/promotion/reddit-r-bashonubuntuonwindows.md @@ -0,0 +1,36 @@ +# r/bashonubuntuonwindows Reddit Post + +## title +native windows alternative to eternal terminal + +## body +i've used eternal terminal on linux for years. on windows, i used to run it inside wsl to connect to servers. booting a full distro just for a persistent shell felt heavy. + +i decided to port the core et protocol to native windows c++. it uses the windows conpty api (same backend as windows terminal) instead of the linux pty. + +why i built this over wsl+et: +- instant start. single `.exe`, no vm boot time +- better integration. copy/paste works natively with windows terminal +- lighter. no vmmem overhead +- scrollback actually works (mosh's broken scrollback was always annoying) +- built-in ui for managing multiple sessions (`--ui` flag) + +features: +- auto-reconnect through network changes +- tmux integration for remote hosts (`--tmux` flag) +- predictive echo for high-latency connections +- ipv6 support +- ssh bootstrap mode (starts server over ssh) + +if you use wsl mainly for ssh persistence, this might be a lighter option. + +repo: https://github.com/microck/undyingterminal +site: https://undyingterminal.com + +## tags for seo +wsl alternative, eternal terminal windows, native windows ssh, windows terminal, conpty, persistent ssh, windows subsystem for linux + +## posting tips +- emphasize "native" vs "wsl" angle +- wsl users are technical, lean into the engineering details +- mention it's complementary to wsl, not a replacement diff --git a/landing/promotion/reddit-r-commandline.md b/landing/promotion/reddit-r-commandline.md new file mode 100644 index 0000000..661ee15 --- /dev/null +++ b/landing/promotion/reddit-r-commandline.md @@ -0,0 +1,32 @@ +# r/commandline Reddit Post + +## title +my ssh sessions kept dying on windows, so i ported eternal terminal to c++ + +## body +i spend half my life in `ssh` + `tmux`. on linux, mosh and eternal terminal keep sessions alive when wifi drops or i close my laptop. + +windows has always been painful. mosh scrollback is broken. et requires wsl. booting a full linux distro just to connect to a server felt wrong. + +so i spent a few months porting et to native windows c++. it uses the windows pty apis (conpty) directly. + +what it does: +- auto-reconnect. disconnect internet, drive home, session resumes. +- native `.exe`. no wsl needed. +- jump host support. +- scrollback that actually works. +- predictive echo for high-latency links. + +it's mit licensed. pty handling on windows is weird. let me know if you hit edge cases. + +repo: https://github.com/microck/undyingterminal +docs: https://undyingterminal.com/docs +site: https://undyingterminal.com + +## tags for seo +eternal terminal, mosh alternative, windows terminal, persistent ssh, reconnectable shell, conpty, windows subsystem for linux + +## posting tips +- post on weekday mornings for max visibility +- respond to every comment within first hour +- cross-post to r/cpp if technical discussion emerges diff --git a/landing/promotion/reddit-r-cpp.md b/landing/promotion/reddit-r-cpp.md new file mode 100644 index 0000000..0247db4 --- /dev/null +++ b/landing/promotion/reddit-r-cpp.md @@ -0,0 +1,36 @@ +# r/cpp Reddit Post + +## title +porting a linux terminal emulator to windows (c++, cmake, vcpkg) + +## body +i recently finished porting "eternal terminal" (a mosh-like tool) to windows. the original code depended heavily on linux-specific pty apis, so bringing it to windows involved some engineering. + +the technical challenges: + +1. conpty integration. i replaced the backend with the windows pseudo console api to render modern terminal sequences on windows 10/11. pipe handling here was interesting. + +2. dependency management. i used `vcpkg` for dependencies (protobuf, libsodium, abseil). it made cross-platform compilation much saner than manual linking. + +3. socket recoverability. the core logic uses a custom tcp protocol that handles "roaming" (client ip changing) without dropping state. had to handle windows socket quirks. + +4. named pipes. windows uses named pipes instead of unix domain sockets for local communication between server and terminal processes. + +what it does: +- keeps ssh sessions alive through network drops +- auto-reconnects without losing scrollback +- supports jump hosts and port forwarding +- native `.exe`, no cygwin or msys2 + +it's mit licensed. if anyone wants to review the conpty implementation or has feedback on the cmake setup, i'd appreciate it. + +repo: https://github.com/microck/undyingterminal + +## tags for seo +windows conpty, c++ terminal emulator, cmake windows, vcpkg, named pipes, eternal terminal port + +## posting tips +- focus on the engineering, not the marketing +- expect questions about conpty vs winpty +- be ready to discuss protobuf/sodium choices +- mention it's single-platform (windows) by design diff --git a/landing/promotion/reddit-r-powershell.md b/landing/promotion/reddit-r-powershell.md new file mode 100644 index 0000000..2754475 --- /dev/null +++ b/landing/promotion/reddit-r-powershell.md @@ -0,0 +1,34 @@ +# r/PowerShell Reddit Post + +## title +stop ssh sessions from dying when your vpn drops + +## body +i've been frustrated for years by how fragile remote sessions are on windows. if my vpn flickers or my laptop sleeps, the shell dies. + +i know `tmux` preserves state on the server. but i wanted the connection itself to survive. there's a linux tool called "eternal terminal" that does this, but it never had a native windows port. + +i built one. it wraps `cmd.exe` or `powershell.exe` on the server (or connects to linux servers) and keeps the tcp connection alive through network changes. + +the main benefit: +you run the client on windows. you can reboot your router, and the terminal window pauses. when internet returns, it resumes. no re-typing passwords. + +features: +- native windows conpty integration (no wsl needed) +- ssh config parsing (`~/.ssh/config`) +- jump host support via `ProxyJump` +- agent forwarding (`-A` flag) +- port forwarding (local and reverse tunnels) + +it's fully open source. if you manage unstable remote connections, i'd appreciate feedback on whether this helps your workflow. + +repo: https://github.com/microck/undyingterminal +download: https://github.com/microck/undyingterminal/releases/latest + +## tags for seo +powershell remoting, windows ssh, eternal terminal windows, persistent shell, vpn disconnect, ssh session timeout, windows terminal + +## posting tips +- frame as solving admin pain, not self-promo +- mention specific use cases (azure, aws, on-prem) +- check if there's a "tool tuesday" or similar thread first diff --git a/landing/promotion/reddit-strategy.md b/landing/promotion/reddit-strategy.md new file mode 100644 index 0000000..25473a3 --- /dev/null +++ b/landing/promotion/reddit-strategy.md @@ -0,0 +1,106 @@ +# Reddit Promotion Strategy for Undying Terminal + +## Target Communities (in order of priority) + +### 1. r/bashonubuntuonwindows (WSL Users) +**Why:** These users already know Eternal Terminal and feel the pain of WSL overhead. +**Best time to post:** Tuesday-Thursday, 9-11 AM EST +**Angle:** "Native alternative that doesn't need WSL" + +### 2. r/PowerShell (Windows Admins) +**Why:** Direct target audience who deals with SSH on Windows daily. +**Best time to post:** Monday or Wednesday, 10 AM EST +**Angle:** "Solves VPN disconnect pain" +**Note:** Check for "Self-Promotion Saturday" threads if main post gets removed. + +### 3. r/commandline (CLI Enthusiasts) +**Why:** Broad reach, technical audience. +**Best time to post:** Any weekday morning +**Angle:** "Fixed a problem I had" + +### 4. r/cpp (C++ Developers) +**Why:** Engineering credibility, potential contributors. +**Best time to post:** Weekend (more technical discussion time) +**Angle:** "Here's how I solved these technical challenges" + +## SEO Keywords to Include + +Primary keywords (use in every post): +- eternal terminal +- windows terminal +- persistent ssh +- reconnectable shell +- mosh alternative + +Secondary keywords (sprinkle in): +- windows conpty +- ssh session timeout +- vpn disconnect +- windows subsystem for linux (wsl) +- jump host +- port forwarding +- native windows + +## Posting Rules Checklist + +- [ ] Wait 9-12 hours between posts (don't spam) +- [ ] Respond to every comment in first 2 hours +- [ ] Don't use url shorteners +- [ ] Include GitHub link in body, not just title +- [ ] Check subreddit rules before posting +- [ ] Have 5-10 genuine comments in subreddit history first + +## Cross-Posting Strategy + +1. **Week 1:** Post to r/bashonubuntuonwindows (most receptive audience) +2. **Week 1:** Post to r/PowerShell (if WSL post gets good engagement) +3. **Week 2:** Post to r/commandline +4. **Week 2:** Post to r/cpp +5. **Week 3+:** Monitor for "ssh disconnect" threads and comment with solution + +## Engagement Templates + +### When someone asks "how is this different from tmux?" +> tmux keeps the session alive on the server, but your ssh connection still dies. undying terminal keeps the connection itself alive, so you don't need to re-attach. plus it has predictive echo for high-latency links. + +### When someone says "just use mosh" +> mosh works but has broken scrollback on windows. this uses native windows conpty apis, so scrollback works properly and it integrates with windows terminal. + +### When someone asks "why not just use wsl?" +> wsl works but has overhead. this is a single native exe with no vm boot time. it's not a replacement for wsl, just a lighter option if all you need is persistent ssh. + +## Metrics to Track + +- github stars increase +- release download count +- docs site traffic (check plausible) +- issues/discussions opened +- reddit post upvote ratio (aim for >70%) + +## Backup Communities (if main posts do well) + +- r/sysadmin (only post in "self-promotion saturday" threads) +- r/selfhosted (if you write about the server architecture) +- r/programming (only if post goes viral elsewhere first) +- r/sideproject (more casual, good for feedback) + +## What NOT to Do + +- Don't post to r/sysadmin main feed (will get removed) +- Don't post all at once (looks spammy) +- Don't ignore comments (hurts engagement algorithm) +- Don't use promotional language ("revolutionary", "game-changing") +- Don't argue in comments (stay helpful) + +## Success Metrics + +Good post: +- 50+ upvotes +- 10+ comments +- 5+ github stars from reddit traffic + +Viral post: +- 200+ upvotes +- 50+ comments +- 20+ github stars +- mentioned in other threads diff --git a/landing/src/app/globals.css b/landing/src/app/globals.css index 185c73a..96bd8b2 100644 --- a/landing/src/app/globals.css +++ b/landing/src/app/globals.css @@ -176,50 +176,10 @@ body { 0 -0.65px rgba(255, 255, 255, 0.35); } -/* Heavier fill for shiny stage (no stroke so gradient is visible) */ -.hero-title-pixel-shine { - font-weight: 900; - letter-spacing: 0.01em; - text-shadow: - 0 0 1px rgba(255, 255, 255, 0.9), - 0 0 3px rgba(255, 255, 255, 0.45), - 0.9px 0 rgba(255, 255, 255, 0.35), - -0.9px 0 rgba(255, 255, 255, 0.35), - 0 0.9px rgba(255, 255, 255, 0.25), - 0 -0.9px rgba(255, 255, 255, 0.25); -} - .hero-title-encrypted { color: rgba(232, 230, 227, 0.45); } -/* Shiny text animation (CSS-driven for reliability) */ -.shiny-text { - background-image: linear-gradient( - var(--shiny-spread, 120deg), - var(--shiny-color, #b5b5b5) 0%, - var(--shiny-color, #b5b5b5) 35%, - var(--shiny-shine, #ffffff) 50%, - var(--shiny-color, #b5b5b5) 65%, - var(--shiny-color, #b5b5b5) 100% - ); - background-size: 200% auto; - background-position: 150% center; - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - color: transparent; -} - -@keyframes shiny-text-move { - 0% { - background-position: 150% center; - } - 100% { - background-position: -50% center; - } -} - /* ── Background layers ── */ .bg-dot-grid { position: fixed; diff --git a/landing/src/app/layout.tsx b/landing/src/app/layout.tsx index 6ee2d7d..cdebc5b 100644 --- a/landing/src/app/layout.tsx +++ b/landing/src/app/layout.tsx @@ -6,33 +6,20 @@ export const viewport: Viewport = { }; export const metadata: Metadata = { - metadataBase: new URL("https://undyingterminal.com"), title: "Undying Terminal", description: "Your session survives. Windows-native persistent terminal that reconnects through disconnects, sleep, and network changes.", keywords: ["terminal", "ssh", "persistent", "windows", "reconnect", "shell"], authors: [{ name: "Microck" }], - alternates: { - canonical: "/", - }, openGraph: { title: "Undying Terminal", description: "Your session survives.", type: "website", url: "https://undyingterminal.com", - images: [ - { - url: "/icon-512.png", - width: 512, - height: 512, - alt: "Undying Terminal", - }, - ], }, twitter: { card: "summary_large_image", title: "Undying Terminal", description: "Your session survives.", - images: ["/icon-512.png"], }, manifest: "/manifest.json", icons: { diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx index bd5b364..a48fbb9 100644 --- a/landing/src/app/page.tsx +++ b/landing/src/app/page.tsx @@ -11,7 +11,6 @@ import FooterSection from "@/components/footer"; import { LightRays } from "@/components/ui/light-rays"; import DecryptedText from "@/components/ui/decrypted-text"; import { ShinyText } from "@/components/ui/shiny-text"; -import { TerminalSkeleton } from "@/components/terminal-skeleton"; import SmoothDropdown from "@/components/smooth-dropdown"; gsap.registerPlugin(TextPlugin); @@ -63,16 +62,6 @@ export default function Home() { const [threadsActive, setThreadsActive] = useState(false); const [heroDecryptDone, setHeroDecryptDone] = useState(false); - const [isTerminalLoading, setIsTerminalLoading] = useState(true); - - // Fail-safe: if decrypt effect doesn't fire (e.g., IO edge-cases), switch to shiny. - useEffect(() => { - if (heroDecryptDone) return; - const t = setTimeout(() => { - setHeroDecryptDone(true); - }, 1600); - return () => clearTimeout(t); - }, [heroDecryptDone]); const handleHeroDecryptDone = useCallback(() => { setHeroDecryptDone(true); @@ -131,18 +120,6 @@ export default function Home() { return () => mql.removeEventListener("change", onChange); }, []); - // Simulate terminal loading state - useEffect(() => { - if (reducedMotion) { - setIsTerminalLoading(false); - return; - } - const timer = setTimeout(() => { - setIsTerminalLoading(false); - }, 800); - return () => clearTimeout(timer); - }, [reducedMotion]); - // Stop the WebGL background when hero isn't visible (prevents scroll jank). useEffect(() => { if (reducedMotion) { @@ -162,7 +139,6 @@ export default function Home() { // Hero entrance + terminal timeline. useEffect(() => { - if (isTerminalLoading) return; if (reducedMotion) { setStatus("connected"); const term = terminalBodyRef.current; @@ -396,7 +372,7 @@ export default function Home() { return () => { ctx.revert(); }; - }, [isTerminalLoading, reducedMotion, setStatus, typeText]); + }, [reducedMotion, setStatus, typeText]); // Cascading scroll reveals for the rest of the page. useEffect(() => { @@ -556,10 +532,7 @@ export default function Home() { text={TITLE_TEXT} disabled={false} speed={2.8} - color="#cfcfcf" - shineColor="#ffffff" - spread={110} - className={`${GeistPixelSquare.className} hero-title-pixel-shine`} + className={`${GeistPixelSquare.className} hero-title-pixel-heavy`} /> )} @@ -575,76 +548,70 @@ export default function Home() { ref={terminalContainerRef} className="w-full" > - {isTerminalLoading ? ( - - ) : ( - <> -
-
- - {/* Status rail */} -
-
- STATUS -
- -
- - Connected -
- -
-
rtt: n/a
-
retries: 0
-
buffer: persisted
-
-
- - {/* Terminal */} -
-
- UT:// - prod/deploy - transport: - ssh -
- session:persist -
- -
-
+
+
+ + {/* Status rail */} +
+
+ STATUS
-
- - - Download for Windows - - - Quick Start - +
+ + Connected
-

- Windows 10 build 17763+ · MIT License -

- - )} +
+
rtt: n/a
+
retries: 0
+
buffer: persisted
+
+
+ + {/* Terminal */} +
+
+ UT:// + prod/deploy + transport: + ssh +
+ session:persist +
+ +
+
+
+ + + +

+ Windows 10 build 17763+ · MIT License +

diff --git a/landing/src/components/ui/shiny-text.tsx b/landing/src/components/ui/shiny-text.tsx index ebd0ef0..44a23c2 100644 --- a/landing/src/components/ui/shiny-text.tsx +++ b/landing/src/components/ui/shiny-text.tsx @@ -1,6 +1,12 @@ "use client"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { useState, useCallback, useEffect, useRef } from "react"; +import { + motion, + useMotionValue, + useAnimationFrame, + useTransform, +} from "motion/react"; export interface ShinyTextProps { text: string; @@ -30,30 +36,68 @@ export const ShinyText: React.FC = ({ delay = 0, }) => { const [isPaused, setIsPaused] = useState(false); - const style = useMemo(() => { - const duration = Math.max(0.2, speed); - const delaySeconds = Math.max(0, delay); - const playState = disabled || isPaused ? "paused" : "running"; - const dir = direction === "left" ? "normal" : "reverse"; - - return { - // Custom properties consumed by globals.css - ["--shiny-duration" as any]: `${duration}s`, - ["--shiny-delay" as any]: `${delaySeconds}s`, - ["--shiny-spread" as any]: `${spread}deg`, - ["--shiny-color" as any]: color, - ["--shiny-shine" as any]: shineColor, - // Direct animation controls - animationPlayState: playState, - animationName: "shiny-text-move", - animationDuration: `${duration}s`, - animationDelay: `${delaySeconds}s`, - animationDirection: yoyo ? ("alternate" as const) : dir, - animationIterationCount: "infinite", - animationTimingFunction: "linear", - animationFillMode: "both", - } as React.CSSProperties; - }, [color, delay, direction, disabled, isPaused, shineColor, speed, spread, yoyo]); + const progress = useMotionValue(0); + const elapsedRef = useRef(0); + const lastTimeRef = useRef(null); + const directionRef = useRef(direction === "left" ? 1 : -1); + + const animationDuration = speed * 1000; + const delayDuration = delay * 1000; + + useAnimationFrame((time) => { + if (disabled || isPaused) { + lastTimeRef.current = null; + return; + } + + if (lastTimeRef.current === null) { + lastTimeRef.current = time; + return; + } + + const deltaTime = time - lastTimeRef.current; + lastTimeRef.current = time; + + elapsedRef.current += deltaTime; + + if (yoyo) { + const cycleDuration = animationDuration + delayDuration; + const fullCycle = cycleDuration * 2; + const cycleTime = elapsedRef.current % fullCycle; + + if (cycleTime < animationDuration) { + const p = (cycleTime / animationDuration) * 100; + progress.set(directionRef.current === 1 ? p : 100 - p); + } else if (cycleTime < cycleDuration) { + progress.set(directionRef.current === 1 ? 100 : 0); + } else if (cycleTime < cycleDuration + animationDuration) { + const reverseTime = cycleTime - cycleDuration; + const p = 100 - (reverseTime / animationDuration) * 100; + progress.set(directionRef.current === 1 ? p : 100 - p); + } else { + progress.set(directionRef.current === 1 ? 0 : 100); + } + } else { + const cycleDuration = animationDuration + delayDuration; + const cycleTime = elapsedRef.current % cycleDuration; + + if (cycleTime < animationDuration) { + const p = (cycleTime / animationDuration) * 100; + progress.set(directionRef.current === 1 ? p : 100 - p); + } else { + progress.set(directionRef.current === 1 ? 100 : 0); + } + } + }); + + useEffect(() => { + directionRef.current = direction === "left" ? 1 : -1; + elapsedRef.current = 0; + progress.set(0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [direction]); + + const backgroundPosition = useTransform(progress, (p) => `${150 - p * 2}% center`); const handleMouseEnter = useCallback(() => { if (pauseOnHover) setIsPaused(true); @@ -63,14 +107,23 @@ export const ShinyText: React.FC = ({ if (pauseOnHover) setIsPaused(false); }, [pauseOnHover]); + const gradientStyle: React.CSSProperties = { + backgroundImage: `linear-gradient(${spread}deg, ${color} 0%, ${color} 35%, ${shineColor} 50%, ${color} 65%, ${color} 100%)`, + backgroundSize: "200% auto", + WebkitBackgroundClip: "text", + backgroundClip: "text", + WebkitTextFillColor: "transparent", + color: "transparent", + }; + return ( - {text} - + ); };