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          -&nbsp;
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