1import { css } from '@emotion/react'; 2import { spacing } from '@expo/styleguide-base'; 3import { TerminalSquareIcon } from '@expo/styleguide-icons'; 4 5import { Snippet } from '../Snippet'; 6import { SnippetContent } from '../SnippetContent'; 7import { SnippetHeader } from '../SnippetHeader'; 8import { CopyAction } from '../actions/CopyAction'; 9 10import { CODE } from '~/ui/components/Text'; 11 12type TerminalProps = { 13 cmd: string[]; 14 cmdCopy?: string; 15 hideOverflow?: boolean; 16 includeMargin?: boolean; 17 title?: string; 18}; 19 20export const Terminal = ({ 21 cmd, 22 cmdCopy, 23 hideOverflow, 24 includeMargin = true, 25 title = 'Terminal', 26}: TerminalProps) => ( 27 <Snippet css={wrapperStyle} includeMargin={includeMargin}> 28 <SnippetHeader alwaysDark title={title} Icon={TerminalSquareIcon}> 29 {renderCopyButton({ cmd, cmdCopy })} 30 </SnippetHeader> 31 <SnippetContent alwaysDark hideOverflow={hideOverflow} className="grid grid-cols-auto-min-1"> 32 {cmd.map(cmdMapper)} 33 </SnippetContent> 34 </Snippet> 35); 36 37/** 38 * This method attempts to naively generate the basic cmdCopy from the given cmd list. 39 * Currently, the implementation is simple, but we can add multiline support in the future. 40 */ 41function getDefaultCmdCopy(cmd: TerminalProps['cmd']) { 42 const validLines = cmd.filter(line => !line.startsWith('#') && line !== ''); 43 if (validLines.length === 1) { 44 return validLines[0].startsWith('$') ? validLines[0].slice(2) : validLines[0]; 45 } 46 return undefined; 47} 48 49function renderCopyButton({ cmd, cmdCopy }: TerminalProps) { 50 const copyText = cmdCopy || getDefaultCmdCopy(cmd); 51 return copyText && <CopyAction alwaysDark text={copyText} />; 52} 53 54/** 55 * Map all provided lines and render the correct component. 56 * This method supports: 57 * - Render newlines for empty strings 58 * - Render a line with `#` prefix as comment with secondary text 59 * - Render a line without `$` prefix as primary text 60 * - Render a line with `$` prefix, as secondary and primary text 61 */ 62function cmdMapper(line: string, index: number) { 63 const key = `line-${index}`; 64 65 if (line.trim() === '') { 66 return <br key={key} className="select-none" />; 67 } 68 69 if (line.startsWith('#')) { 70 return ( 71 <CODE 72 key={key} 73 className="whitespace-pre !bg-[transparent] !border-none select-none !text-palette-gray10"> 74 {line} 75 </CODE> 76 ); 77 } 78 79 if (line.startsWith('$')) { 80 return ( 81 <div key={key}> 82 <CODE className="whitespace-pre !bg-[transparent] !border-none select-none !text-secondary"> 83 - 84 </CODE> 85 <CODE className="whitespace-pre !bg-[transparent] !border-none text-default"> 86 {line.substring(1).trim()} 87 </CODE> 88 </div> 89 ); 90 } 91 92 return ( 93 <CODE key={key} className="whitespace-pre !bg-[transparent] !border-none text-default"> 94 {line} 95 </CODE> 96 ); 97} 98 99const wrapperStyle = css` 100 li & { 101 margin-top: ${spacing[4]}px; 102 display: flex; 103 } 104`; 105