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