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