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