1import { css } from '@emotion/react'; 2import { darkTheme, spacing } from '@expo/styleguide'; 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} css={unselectableStyle} />; 66 } 67 68 if (line.startsWith('#')) { 69 return ( 70 <CODE key={key} css={[codeStyle, unselectableStyle, { color: darkTheme.code.comment }]}> 71 {line} 72 </CODE> 73 ); 74 } 75 76 if (line.startsWith('$')) { 77 return ( 78 <div key={key}> 79 <CODE 80 css={[ 81 codeStyle, 82 unselectableStyle, 83 { display: 'inline', color: darkTheme.text.secondary }, 84 ]}> 85 → 86 </CODE> 87 <CODE css={codeStyle}>{line.substring(1).trim()}</CODE> 88 </div> 89 ); 90 } 91 92 return ( 93 <CODE key={key} css={[codeStyle, { display: 'inherit' }]}> 94 {line} 95 </CODE> 96 ); 97} 98 99const wrapperStyle = css` 100 li & { 101 margin-top: ${spacing[4]}px; 102 display: flex; 103 } 104`; 105 106const unselectableStyle = css` 107 user-select: none; 108`; 109 110const codeStyle = css` 111 white-space: pre; 112 display: inline-block; 113 line-height: 140%; 114 background-color: transparent; 115 border: none; 116 color: ${darkTheme.text.default}; 117`; 118