Skip to main content

Overview

The Terminal component is an interactive, bash-style terminal emulator that serves as the primary navigation interface for the portfolio. It features a custom command system, tab completion, command history, and a virtual file system for browsing projects, work experience, and education.

Features

  • Custom Commands: help, ls, cd, cat, open, ping, git init, whoami, about, clear
  • Tab Completion: Auto-complete commands, directories, and files
  • Command History: Arrow up/down to navigate previous commands
  • Virtual File System: Navigate through ~/proyectos, ~/trabajos, and ~/estudios
  • Interactive Links: Clickable URLs for projects and external resources
  • Embedded Mode: Can run standalone or embedded in a Window
  • Responsive: Adapts prompt and layout for mobile screens

Props

embedded
boolean
default:"false"
Whether the terminal is embedded in a Window component. When true, removes outer container styling and title bar.

Types

Color

type Color = "green" | "cyan" | "yellow" | "red" | "white" | "gray";
Available text colors for terminal output.

Line Types

type Line =
  | { id: number; type: "ascii" }                          // ASCII art header
  | { id: number; type: "blank" }                          // Blank line
  | { id: number; type: "command"; prompt: string; cmd: string }  // User input
  | { id: number; type: "text"; text: string; color?: Color }     // Text output
  | { id: number; type: "link"; label: string; url: string };    // Clickable link

Usage Examples

import Terminal from "./components/Terminal";

function App() {
  return <Terminal />;
}

Command System

Available Commands

CommandDescriptionExample
helpDisplay all available commandshelp
whoamiDisplay identity and rolewhoami
aboutShow detailed bioabout
ls / dirList directory contentsls
cd <folder>Change directorycd proyectos
cd ..Go back one directorycd ..
cat <file>Read a filecat portfolio.txt
open <file>Open/download a fileopen cv.pdf
ping <project>Get project info + URLping portfolio
git initShow GitHub profilegit init
clearClear terminal screenclear
bot_javierAI chatbot (upcoming feature)bot_javier

Command Implementation

Commands are executed through the execute() function:
function execute(raw: string) {
  const cmd = raw.trim();
  if (!cmd) return;

  push({ type: "command", prompt: promptStr, cmd });
  setCmdHistory((h) => [cmd, ...h]);

  const [command, ...rest] = cmd.split(/\s+/);
  const arg = rest.join(" ");

  switch (command.toLowerCase()) {
    case "help":    cmdHelp();       break;
    case "whoami":  cmdWhoami();     break;
    case "ls":      cmdDir();        break;
    case "cd":      cmdCd(arg);      break;
    case "cat":     cmdCat(arg);     break;
    case "open":    cmdOpen(arg);    break;
    case "ping":    cmdPing(arg);    break;
    case "clear":   setLines([...BOOT]); break;
    // ...
  }
}

File System Structure

The terminal emulates a Unix-like file system:
~
├── proyectos/
│   ├── portfolio.txt
│   ├── chatbot-ai.txt
│   └── ...
├── trabajos/
│   ├── empresa1.txt
│   ├── empresa2.txt
│   └── ...
└── estudios/
    ├── universidad.txt
    ├── master.txt
    └── ...
~$ ls
  proyectos/    estudios/    trabajos/

~$ cd proyectos
~/proyectos$ ls
  portfolio.txt    chatbot-ai.txt    ecommerce.txt

~/proyectos$ cat portfolio.txt
  Portfolio Personal
  ─────────────────────────────────────────────────
  Descripción: Sitio web interactivo estilo macOS
  Tech:        React · TypeScript · Tailwind CSS
  URL:         https://javiernavas.dev

Tab Completion

Press Tab to auto-complete commands, directories, and files:
function handleTab(e: React.KeyboardEvent<HTMLInputElement>) {
  e.preventDefault();
  if (!input.trim()) return;

  const completions = getCompletions(input);
  
  if (completions.length === 1) {
    // Single match → complete it
    const parts = input.split(/\s+/);
    if (parts.length === 1) {
      setInput(completions[0] + " ");
    } else {
      parts[parts.length - 1] = completions[0];
      setInput(parts.join(" ") + " ");
    }
  } else if (completions.length > 1) {
    // Multiple matches → show options
    push(
      { type: "command", prompt: promptStr, cmd: input },
      { type: "text", text: completions.join("    "), color: "cyan" },
      { type: "blank" }
    );
  }
}

Completion Logic

// Complete command names
if (parts.length === 1) {
  const partial = parts[0].toLowerCase();
  return COMMANDS.filter((c) => c.startsWith(partial));
}

Command History

Navigate previous commands with arrow keys:
function handleKey(e: KeyboardEvent<HTMLInputElement>) {
  if (e.key === "ArrowUp") {
    e.preventDefault();
    const idx = Math.min(historyIdx + 1, cmdHistory.length - 1);
    setHistoryIdx(idx);
    if (cmdHistory[idx] !== undefined) setInput(cmdHistory[idx]);
  } else if (e.key === "ArrowDown") {
    e.preventDefault();
    const idx = Math.max(historyIdx - 1, -1);
    setHistoryIdx(idx);
    setInput(idx === -1 ? "" : (cmdHistory[idx] ?? ""));
  }
}
History is stored in state:
const [cmdHistory, setCmdHistory] = useState<string[]>([]);
const [historyIdx, setHistoryIdx] = useState(-1);

// Add to history on execute
setCmdHistory((h) => [cmd, ...h]);

Line Rendering

Each line type renders differently:
function RenderLine({ line }: { line: Line }) {
  if (line.type === "blank") {
    return <div className="h-2" />;
  }

  if (line.type === "ascii") {
    return (
      <pre className="text-cyan-400 leading-tight ...">
        {ASCII}
      </pre>
    );
  }

  if (line.type === "command") {
    return (
      <div className="flex gap-2">
        <span className="text-green-400">{line.prompt}</span>
        <span className="text-gray-100">{line.cmd}</span>
      </div>
    );
  }

  if (line.type === "link") {
    return (
      <a href={line.url} target="_blank" className="text-cyan-400 underline ...">
        {line.label}
      </a>
    );
  }

  // text
  const cls = line.color ? COLOR_MAP[line.color] : "text-gray-300";
  return <div className={cls}>{line.text}</div>;
}

Color Mapping

const COLOR_MAP: Record<string, string> = {
  green:  "text-green-400",
  cyan:   "text-cyan-400",
  yellow: "text-yellow-400",
  red:    "text-red-400",
  white:  "text-gray-100",
  gray:   "text-gray-500",
};

ASCII Art Header

Bootup sequence displays ASCII art:
const ASCII = `
     ██╗ █████╗ ██╗   ██╗██╗███████╗██████╗
     ██║██╔══██╗██║   ██║██║██╔════╝██╔══██╗
     ██║███████║██║   ██║██║█████╗  ██████╔╝
██   ██║██╔══██║╚██╗ ██╔╝██║██╔══╝  ██╔══██╗
╚█████╔╝██║  ██║ ╚████╔╝ ██║███████╗██║  ██║
 ╚════╝ ╚═╝  ╚═╝  ╚═══╝  ╚═╝╚══════╝╚═╝  ╚═╝

███╗   ██╗ █████╗ ██╗   ██╗ █████╗ ███████╗
████╗  ██║██╔══██╗██║   ██║██╔══██╗██╔════╝
██╔██╗ ██║███████║██║   ██║███████║███████╗
██║╚██╗██║██╔══██║╚██╗ ██╔╝██╔══██║╚════██║
██║ ╚████║██║  ██║ ╚████╔╝ ██║  ██║███████║
╚═╝  ╚═══╝╚═╝  ╚═╝  ╚═══╝  ╚═╝  ╚═╝╚══════╝`;

const BOOT: Line[] = [
  { id: nextId(), type: "ascii" },
  { id: nextId(), type: "blank" },
  { id: nextId(), type: "text", text: "Bienvenido al portfolio de Javier Navas.  (v1.0.0)", color: "white" },
  { id: nextId(), type: "text", text: SEP, color: "gray" },
  { id: nextId(), type: "text", text: "Escribe `help` para ver los comandos disponibles.", color: "gray" },
  { id: nextId(), type: "blank" },
];

Prompt Customization

The prompt updates based on current directory:
const [path, setPath] = useState("~");
const { prompt: host } = textos.terminal; // "[email protected]"
const promptStr = `${host}:${path}$`;

// Examples:
// [email protected]:~$
// [email protected]:~/proyectos$
// [email protected]:~/trabajos$

Auto-scroll

Terminal automatically scrolls to bottom on new output:
const bottomRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [lines]);

// In render:
<div ref={bottomRef} />

Mobile Responsiveness

The terminal adapts for smaller screens:
<span className="text-green-400">
  <span className="hidden md:inline">{host}:</span>
  {shortPrompt}
</span>
On mobile, shows ~$ instead of full [email protected]:~$.

Styling

Terminal uses a dark monospace theme:
<div className="
  flex flex-col h-full 
  bg-[#111111]            /* Deep black background */
  font-mono               /* Monospace font */
  text-xs md:text-sm      /* Responsive text size */
  leading-relaxed         /* Comfortable line height */
">

Input Field

<input
  className="
    flex-1 
    bg-transparent          /* No background */
    text-gray-100          /* Light text */
    outline-none           /* No focus ring */
    caret-cyan-400         /* Colored cursor */
    min-w-0                /* Prevent overflow */
  "
  spellCheck={false}
  autoComplete="off"
  autoCorrect="off"
  autoCapitalize="none"
/>

Integration with Content

The terminal reads data from textos.terminal:
import { textos } from "../textos";

const { proyectos, trabajos, estudios, about, prompt } = textos.terminal;
This allows easy content updates without modifying component logic.

Focus Management

Terminal auto-focuses on mount (desktop only):
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
  if (window.innerWidth >= 768) inputRef.current?.focus();
}, []);
Clicking anywhere in the terminal focuses the input:
<div onClick={() => inputRef.current?.focus()}>
  {/* Terminal content */}
</div>
  • Window - Container for embedded terminal
  • Desktop - Manages terminal window state
  • Dock - Launches terminal window
  • MenuBar - Top bar above terminal

Build docs developers (and LLMs) love