Skip to main content
Glama
template.tsx.j210.2 kB
import React from 'react'; import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion'; interface CommandBlock { command: string; output: string; typeOn: boolean; } interface TerminalProps { startFrame: number; durationInFrames: number; commands: CommandBlock[] | string; prompt: 'bash' | 'zsh' | 'powershell' | 'custom'; customPrompt: string; title: string; theme: 'dark' | 'light' | 'dracula' | 'monokai' | 'nord' | 'solarized'; width: number; height: number; position: string; showCursor: boolean; typeSpeed: number; } export const Terminal: React.FC<TerminalProps> = ({ startFrame, durationInFrames, commands = [], prompt = 'bash', customPrompt = '$', title = 'Terminal', theme = 'dark', width = 900, height = 600, position = 'center', showCursor = true, typeSpeed = 0.05, }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); // Visibility check if (frame < startFrame || frame >= startFrame + durationInFrames) { return null; } const relativeFrame = frame - startFrame; // Parse commands if it's a string const parsedCommands: CommandBlock[] = typeof commands === 'string' ? JSON.parse(commands) : commands; // Theme-specific variations using design tokens const isDark = theme !== 'light'; // Prompt variants const promptStrings: Record<string, string> = { bash: '$ ', zsh: '❯ ', powershell: 'PS> ', custom: customPrompt + ' ', }; const promptStr = promptStrings[prompt]; // Position mappings const positionMap: Record<string, { top?: string; left?: string; right?: string; bottom?: string; transform?: string }> = { center: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', }, 'top-left': { top: '[[ spacing.spacing.xl ]]', left: '[[ spacing.spacing.xl ]]' }, 'top-center': { top: '[[ spacing.spacing.xl ]]', left: '50%', transform: 'translateX(-50%)' }, 'top-right': { top: '[[ spacing.spacing.xl ]]', right: '[[ spacing.spacing.xl ]]' }, 'center-left': { top: '50%', left: '[[ spacing.spacing.xl ]]', transform: 'translateY(-50%)' }, 'center-right': { top: '50%', right: '[[ spacing.spacing.xl ]]', transform: 'translateY(-50%)' }, 'bottom-left': { bottom: '[[ spacing.spacing.xl ]]', left: '[[ spacing.spacing.xl ]]' }, 'bottom-center': { bottom: '[[ spacing.spacing.xl ]]', left: '50%', transform: 'translateX(-50%)' }, 'bottom-right': { bottom: '[[ spacing.spacing.xl ]]', right: '[[ spacing.spacing.xl ]]' }, }; // Entrance animation const entrance = spring({ frame: relativeFrame, fps: fps, config: { damping: [[ motion.default_spring.config.damping ]], stiffness: [[ motion.default_spring.config.stiffness ]], mass: [[ motion.default_spring.config.mass ]], }, }); const opacity = interpolate(relativeFrame, [0, 15], [0, 1], { extrapolateRight: 'clamp' }); const scaleAnimation = interpolate(entrance, [0, 1], [0.95, 1]); // Typing animation helper const getTypedText = (text: string, startFrame: number, speed: number): string => { const charsToShow = Math.floor((relativeFrame - startFrame) / (fps * speed)); return text.slice(0, Math.max(0, charsToShow)); }; // Calculate frame timing for each command let currentFrameOffset = 20; // Start after entrance animation const commandTimings: Array<{ cmdStartFrame: number; cmdEndFrame: number; outputStartFrame: number }> = []; parsedCommands.forEach((cmd) => { const cmdStartFrame = currentFrameOffset; const typingDuration = cmd.typeOn ? cmd.command.length * fps * typeSpeed : 0; const cmdEndFrame = cmdStartFrame + typingDuration; const outputStartFrame = cmdEndFrame + 5; // Small pause before output commandTimings.push({ cmdStartFrame, cmdEndFrame, outputStartFrame }); // Move offset for next command currentFrameOffset = outputStartFrame + 30; // Pause after output }); // Cursor blinking const cursorVisible = showCursor && Math.floor(relativeFrame / 15) % 2 === 0; return ( <AbsoluteFill style={{ ...positionMap[position], width, height, opacity, transform: `${positionMap[position].transform || ''} scale(${scaleAnimation})`, }} > <div style={{ width: '100%', height: '100%', background: isDark ? '[[ colors.background.darker ]]' : '[[ colors.background.light ]]', borderRadius: '[[ spacing.border_radius.lg ]]', overflow: 'hidden', filter: `drop-shadow(0 10px 40px [[ colors.shadow.dark ]])`, display: 'flex', flexDirection: 'column', }} > {/* Terminal header */} <div style={{ height: `${parseInt('[[ spacing.spacing['2xl'] ]]')}px`, background: isDark ? '[[ colors.background.darker ]]' : '[[ colors.background.light ]]', display: 'flex', alignItems: 'center', padding: '0 [[ spacing.spacing.md ]]', borderBottom: `1px solid [[ colors.border.medium ]]`, }} > {/* Window controls */} <div style={{ display: 'flex', gap: '[[ spacing.spacing.xs ]]', marginRight: '[[ spacing.spacing.md ]]' }}> <div style={{ width: parseInt('[[ spacing.border_radius.lg ]]'), height: parseInt('[[ spacing.border_radius.lg ]]'), borderRadius: '50%', background: '[[ colors.semantic.error ]]' }} /> <div style={{ width: parseInt('[[ spacing.border_radius.lg ]]'), height: parseInt('[[ spacing.border_radius.lg ]]'), borderRadius: '50%', background: '[[ colors.semantic.warning ]]' }} /> <div style={{ width: parseInt('[[ spacing.border_radius.lg ]]'), height: parseInt('[[ spacing.border_radius.lg ]]'), borderRadius: '50%', background: '[[ colors.semantic.success ]]' }} /> </div> {/* Title */} <div style={{ color: isDark ? '[[ colors.text.on_dark ]]' : '[[ colors.text.on_light ]]', fontSize: parseInt('[[ typography.font_sizes[typography.default_resolution].base ]]'), fontFamily: "'[[ "', '".join(typography.body_font.fonts) ]]'", opacity: 0.8, }} > {title} </div> </div> {/* Terminal content */} <div style={{ flex: 1, padding: '[[ spacing.spacing.md ]]', fontFamily: "'[[ "', '".join(typography.code_font.fonts) ]]'", fontSize: parseInt('[[ typography.font_sizes[typography.default_resolution].base ]]'), lineHeight: parseFloat('[[ typography.line_heights.relaxed ]]'), overflow: 'auto', }} > {parsedCommands.map((cmd, index) => { const timing = commandTimings[index]; const showCommand = relativeFrame >= timing.cmdStartFrame; const commandComplete = relativeFrame >= timing.cmdEndFrame; const showOutput = relativeFrame >= timing.outputStartFrame; if (!showCommand) return null; const displayedCommand = cmd.typeOn ? getTypedText(cmd.command, timing.cmdStartFrame, typeSpeed) : cmd.command; const showCommandCursor = !commandComplete && cursorVisible; return ( <div key={index} style={{ marginBottom: '[[ spacing.spacing.sm ]]' }}> {/* Command line */} <div style={{ display: 'flex', alignItems: 'flex-start' }}> <span style={{ color: isDark ? '[[ colors.accent[0] ]]' : '[[ colors.primary[2] ]]', marginRight: '[[ spacing.spacing.xs ]]' }}> {promptStr} </span> <span style={{ color: isDark ? '[[ colors.text.on_dark ]]' : '[[ colors.text.on_light ]]' }}> {displayedCommand} {showCommandCursor && ( <span style={{ background: isDark ? '[[ colors.accent[1] ]]' : '[[ colors.primary[1] ]]', width: `${parseInt('[[ spacing.spacing.xs ]]')}px`, height: '1em', display: 'inline-block', marginLeft: `${parseInt('[[ spacing.border_width.medium ]]')}px`, verticalAlign: 'text-bottom', }} /> )} </span> </div> {/* Output */} {showOutput && cmd.output && ( <div style={{ color: isDark ? '[[ colors.accent[2] ]]' : '[[ colors.primary[1] ]]', marginTop: '[[ spacing.spacing.xs ]]', marginLeft: '[[ spacing.spacing.md ]]', whiteSpace: 'pre-wrap', opacity: 0.9, }} > {cmd.output} </div> )} </div> ); })} {/* Final cursor (after all commands) */} {parsedCommands.length > 0 && relativeFrame >= commandTimings[parsedCommands.length - 1].outputStartFrame + 30 && ( <div style={{ display: 'flex', alignItems: 'flex-start' }}> <span style={{ color: isDark ? '[[ colors.accent[0] ]]' : '[[ colors.primary[2] ]]', marginRight: '[[ spacing.spacing.xs ]]' }}> {promptStr} </span> {cursorVisible && ( <span style={{ background: isDark ? '[[ colors.accent[1] ]]' : '[[ colors.primary[1] ]]', width: `${parseInt('[[ spacing.spacing.xs ]]')}px`, height: '1em', display: 'inline-block', verticalAlign: 'text-bottom', }} /> )} </div> )} </div> </div> </AbsoluteFill> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-remotion'

If you have feedback or need assistance with the MCP directory API, please join our Discord server