diff --git a/app/playground/page.tsx b/app/playground/page.tsx index f6a7bde..b4496cc 100644 --- a/app/playground/page.tsx +++ b/app/playground/page.tsx @@ -1,5 +1,5 @@ import { TerminalApp } from '@/components/terminal-app' -import { Terminal, TerminalCommand, TerminalDiff, TerminalOutput, TerminalSpinner, TerminalBadge, ThemeSwitcher } from '@/components/terminal' +import { Terminal, TerminalCommand, TerminalDiff, TerminalOutput, TerminalSpinner, TerminalBadge, ThemeSwitcher, TerminalPager } from '@/components/terminal' import { TerminalProgress } from '@/components/terminal-progress' import { LogDemo } from './log-demo' import { PromptDemo } from './prompt-demo' @@ -72,6 +72,44 @@ export default function PlaygroundPage() { +
+

+ TerminalPager +

+

+ Navigate through long content (like less/more). Use Space/↓ for next, b/↑ for previous. +

+ + cat long-file.log | less + + +
+

Copy Button diff --git a/components/terminal-pager.tsx b/components/terminal-pager.tsx new file mode 100644 index 0000000..a88ebde --- /dev/null +++ b/components/terminal-pager.tsx @@ -0,0 +1,187 @@ +'use client' + +import { useState, useEffect, useCallback, useRef } from 'react' + +export interface TerminalPagerProps { + /** Content to paginate (string or array of lines) */ + content: string | string[] + /** Lines per page (default: 10) */ + pageSize?: number + /** Callback when pager is exited */ + onExit?: () => void + /** Show controls hint (default: true) */ + showControls?: boolean + /** Color variant (default: 'blue') */ + variant?: 'green' | 'blue' | 'yellow' | 'red' | 'purple' | 'cyan' + /** Additional CSS classes */ + className?: string +} + +const variantColors: Record = { + green: 'var(--term-green)', + blue: 'var(--term-blue)', + yellow: 'var(--term-yellow)', + red: 'var(--term-red)', + purple: 'var(--term-purple)', + cyan: 'var(--term-cyan)', +} + +/** + * Navigate through long output with keyboard controls (like `less` or `more`). + * Displays content page-by-page with progress indicator. + * + * @param content - Content to paginate (string or array of lines) + * @param pageSize - Number of lines per page (default: 10) + * @param onExit - Callback when user exits (q or Esc key) + * @param showControls - Display keyboard controls hint (default: true) + * @param variant - Color for progress indicator + * @param className - Additional classes applied to the root element + * + * @example + * ```tsx + * console.log('Pager closed')} + * /> + * + * + * ``` + */ +export function TerminalPager({ + content, + pageSize = 10, + onExit, + showControls = true, + variant = 'blue', + className = '', +}: TerminalPagerProps) { + const [currentPage, setCurrentPage] = useState(0) + const containerRef = useRef(null) + const color = variantColors[variant] + + // Convert content to lines array + const lines = typeof content === 'string' ? content.split('\n') : content + const totalPages = Math.max(1, Math.ceil(lines.length / pageSize)) + const progress = Math.round(((currentPage + 1) / totalPages) * 100) + + // Get current page lines + const startLine = currentPage * pageSize + const endLine = Math.min(startLine + pageSize, lines.length) + const currentLines = lines.slice(startLine, endLine) + + const goToNextPage = useCallback(() => { + setCurrentPage((prev) => Math.min(prev + 1, totalPages - 1)) + }, [totalPages]) + + const goToPrevPage = useCallback(() => { + setCurrentPage((prev) => Math.max(prev - 1, 0)) + }, []) + + const goToFirstPage = useCallback(() => { + setCurrentPage(0) + }, []) + + const goToLastPage = useCallback(() => { + setCurrentPage(totalPages - 1) + }, [totalPages]) + + const handleExit = useCallback(() => { + onExit?.() + }, [onExit]) + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + switch (e.key) { + case ' ': + case 'Enter': + case 'j': + case 'ArrowDown': + e.preventDefault() + goToNextPage() + break + case 'b': + case 'k': + case 'ArrowUp': + e.preventDefault() + goToPrevPage() + break + case 'g': + e.preventDefault() + goToFirstPage() + break + case 'G': + e.preventDefault() + goToLastPage() + break + case 'q': + case 'Escape': + e.preventDefault() + handleExit() + break + } + }, + [goToNextPage, goToPrevPage, goToFirstPage, goToLastPage, handleExit], + ) + + useEffect(() => { + const container = containerRef.current + if (!container) return + + // Focus container on mount for keyboard events + container.focus() + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [handleKeyDown]) + + return ( +
+ {/* Content */} +
+ {currentLines.map((line, i) => ( +
+ {line} +
+ ))} +
+ + {/* Progress indicator */} + {totalPages > 1 && ( +
+ -- More -- + + ({progress}% · {currentPage + 1}/{totalPages}) + +
+ )} + + {/* Controls hint */} + {showControls && ( +
+ {totalPages > 1 ? ( + <> + + Space/↓: next · b/↑: back · g: first · G: last · q: quit + + + ) : ( + q: quit + )} +
+ )} +
+ ) +} diff --git a/components/terminal.tsx b/components/terminal.tsx index 56c9e1f..7c69641 100644 --- a/components/terminal.tsx +++ b/components/terminal.tsx @@ -264,3 +264,4 @@ export { TerminalAutocomplete, useAutocomplete, COMMON_COMMANDS, COMMON_FLAGS, f export { TerminalGhosttyTheme, GhosttyThemePicker } from './terminal-ghostty' export { ThemeSwitcher } from './theme-switcher' export { TerminalBadge } from './terminal-badge' +export { TerminalPager } from './terminal-pager'