Skip to content

Commit

Permalink
feat: added support for commands and going back in history
Browse files Browse the repository at this point in the history
  • Loading branch information
cecilia-sanare committed Aug 21, 2023
1 parent a7ce9bb commit 390aee8
Show file tree
Hide file tree
Showing 15 changed files with 525 additions and 26 deletions.
31 changes: 31 additions & 0 deletions app/components/common/Prism.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';

export type PrismProps = {
className?: string;
children: string | string[];
};

export function Prism({ className, children }: PrismProps) {
return (
<div className={className}>
<SyntaxHighlighter
language="shell"
style={tomorrow}
customStyle={{ backgroundColor: 'transparent', padding: 0, margin: 0, font: 'inherit', fontSize: 'inherit' }}
codeTagProps={{
style: {
font: 'inherit',
},
}}
lineProps={{
style: {
font: 'inherit',
},
}}
>
{children}
</SyntaxHighlighter>
</div>
);
}
2 changes: 1 addition & 1 deletion app/components/common/Typography.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function Typography<
const Element: React.ElementType = as ?? type ?? TypographyDefaultElement;

const className = useReadOnlyCachedState(() => {
return classnames(styles.typography, styles[type], externalClassName);
return classnames(styles.typography, styles[type as string], externalClassName);
}, [type, externalClassName]);

return (
Expand Down
1 change: 1 addition & 0 deletions app/components/terminal/WSH.module.scss
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.shell {
font-family: 'Roboto Mono', monospace;
}
7 changes: 6 additions & 1 deletion app/components/terminal/WSH.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Window } from '../web-osx/Window';
import { WSHInput } from './WSHInput';

export type WSHProps = {
demo?: boolean;
};

export function WSH({ demo }: WSHProps) {
return <Window preventEvents={demo} />;
return (
<Window preventEvents={demo}>
<WSHInput />
</Window>
);
}
37 changes: 37 additions & 0 deletions app/components/terminal/WSHInput.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.container {
position: relative;
font-family: 'Roboto Mono', monospace;
font-size: 16px;
margin-left: 20px;
line-height: 1.5;

.prism {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}

&:before {
content: '$';
user-select: none;
position: absolute;
top: 0;
left: -20px;
}

.input {
color: transparent;
caret-color: white;
background: transparent;
padding: 0;
border: none;
width: 100%;
height: 100%;
outline: none;
font: inherit;
resize: none;
}
}
65 changes: 65 additions & 0 deletions app/components/terminal/WSHInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { KeyboardEvent, useCallback, useState } from 'react';
import * as styles from './WSHInput.module.scss';
import { History } from '../../services/history';
import { Prism } from '../common/Prism';

export type WSHInputProps = {
onSubmit?: (value: string) => void;
};

export function WSHInput({ onSubmit }: WSHInputProps) {
const [value, setValue] = useState('');

const onSubmitInternal = useCallback(
(e: KeyboardEvent<HTMLTextAreaElement>, value: string, ignore: boolean = false) => {
e.preventDefault();

if (!ignore) {
History.add(value);
}

onSubmit?.(value);
setValue('');
},
[onSubmit]
);

return (
<div className={styles.container}>
<Prism className={styles.prism}>{value}</Prism>
<textarea
className={styles.input}
spellCheck={false}
value={value}
onChange={({ target }) => setValue(target.value)}
onKeyDown={(e) => {
const { metaKey, ctrlKey, key } = e;

if (metaKey && key === 'k') {
// CMD + K - Clears out the terminal window
onSubmitInternal(e, 'clear', true);
} else if (ctrlKey && key === 'u') {
// Ctrl + U - Clears out the current line
e.preventDefault();

setValue('');
} else if (key === 'ArrowUp') {
// Up - Navigates to the prior history
e.preventDefault();

setValue(History.next());
} else if (key === 'ArrowDown') {
// Up - Navigates to more current history
e.preventDefault();

setValue(History.previous() ?? '');
} else if (key === 'Enter') {
// Enter - Submits the current input

onSubmitInternal(e, value);
}
}}
/>
</div>
);
}
10 changes: 10 additions & 0 deletions app/components/web-osx/Window.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@
}

.window {
display: flex;
flex-direction: column;
flex: 1;
border-radius: 6px;
background-color: rgb(27, 29, 35);
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 20px;
z-index: 9999;
}

.content {
position: relative;
display: flex;
flex-direction: column;
flex: 1;
margin: 15px;
}
42 changes: 24 additions & 18 deletions app/components/web-osx/Window.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,35 @@ import * as styles from './Window.module.scss';
import { Menu } from './Menu';
import classNames from 'classnames';
import { WindowResizer } from './WindowResizer';
import { isNullable } from '../../utils/guards';

export type WindowProps = {
children?: ReactNode;
preventEvents?: boolean;
};

export function Window({ children, preventEvents }: WindowProps) {
const containerRef = useRef<HTMLDivElement>();
const windowRef = useRef<HTMLDivElement>();
const containerRef = useRef<HTMLDivElement>(null);
const windowRef = useRef<HTMLDivElement>(null);
const [zoom, setZoom] = useState(false);

const className = useReadOnlyCachedState(() => {
return classNames(styles.container, preventEvents && styles.preventEvents);
}, [preventEvents]);

useDidUpdateEffect(() => {
const { top, left, right, bottom } = containerRef.current.getBoundingClientRect();
if (isNullable(containerRef.current) || isNullable(windowRef.current)) return;

const containerElement = containerRef.current;
const windowElement = windowRef.current;

const { top, left, right, bottom } = containerElement.getBoundingClientRect();

if (zoom) {
windowRef.current.style.position = 'fixed';
windowElement.style.position = 'fixed';
}

windowRef.current
windowElement
.animate(
[
{
Expand All @@ -52,27 +58,27 @@ export function Window({ children, preventEvents }: WindowProps) {
)
.finished.then(() => {
if (zoom) {
windowRef.current.style.borderRadius = '0';
windowRef.current.style.top = '0';
windowRef.current.style.left = '0';
windowRef.current.style.right = '0';
windowRef.current.style.bottom = '0';
windowElement.style.borderRadius = '0';
windowElement.style.top = '0';
windowElement.style.left = '0';
windowElement.style.right = '0';
windowElement.style.bottom = '0';
} else {
windowRef.current.style.borderRadius = '';
windowRef.current.style.position = '';
windowRef.current.style.top = '';
windowRef.current.style.left = '';
windowRef.current.style.right = '';
windowRef.current.style.bottom = '';
windowElement.style.borderRadius = '';
windowElement.style.position = '';
windowElement.style.top = '';
windowElement.style.left = '';
windowElement.style.right = '';
windowElement.style.bottom = '';
}
});
}, [zoom]);
}, [zoom, containerRef, windowRef]);

return (
<WindowResizer className={className} ref={containerRef} debug>
<div className={styles.window} ref={windowRef}>
<Menu onZoom={() => setZoom(!zoom)} />
{children}
<div className={styles.content}>{children}</div>
</div>
</WindowResizer>
);
Expand Down
13 changes: 9 additions & 4 deletions app/components/web-osx/WindowResizer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { ReactNode, useState, ComponentProps, useRef, useEffect, forwardRef } from 'react';
import React, { ReactNode, useState, ComponentProps, useRef, useEffect, forwardRef, ForwardedRef } from 'react';
import { useCachedState, useReadOnlyCachedState } from '@rain-cafe/react-utils';
import * as styles from './WindowResizer.module.scss';
import classNames from 'classnames';
import { isNullable } from '../../utils/guards';

export type WindowResizerProps = {
children: ReactNode;
Expand All @@ -20,18 +21,22 @@ export const WindowResizer = forwardRef(function WindowResizer(
height: externalHeight,
...props
}: WindowResizerProps,
externalRef: React.MutableRefObject<HTMLDivElement>
externalRef: ForwardedRef<HTMLDivElement>
) {
const className = useReadOnlyCachedState(() => {
return classNames(styles.container, externalClassName);
}, [externalClassName]);
const [height, setHeight] = useCachedState(() => externalHeight, [externalHeight]);
const [width, setWidth] = useCachedState(() => externalWidth, [externalWidth]);
const [resizer, setResizer] = useState<HTMLDivElement | null>(null);
const containerRef: React.MutableRefObject<HTMLDivElement> = externalRef || useRef<HTMLDivElement>();
const containerRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
typeof externalRef === 'function' ? null : externalRef
);

useEffect(() => {
const onResize = (e: MouseEvent) => {
if (isNullable(resizer) || isNullable(containerRef.current)) return;

if (resizer.classList.contains(styles.right) || resizer.classList.contains(styles.corner)) {
setWidth(e.clientX - containerRef.current.getBoundingClientRect().left);
}
Expand All @@ -52,7 +57,7 @@ export const WindowResizer = forwardRef(function WindowResizer(
return () => window.removeEventListener('mousemove', onResize);
}, [resizer]);

const onBeginResize = (e) => {
const onBeginResize = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
setResizer(e.currentTarget);

Expand Down
71 changes: 71 additions & 0 deletions app/services/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ScopedStorage } from './storage';
// import { Bubbles } from '../dynamic/utils-bubbles.js';

export class ScopedHistory<T extends string> {
private storage: ScopedStorage<T>;
index: number | null = null;
private _list: string[];

constructor(scope: T) {
this.storage = new ScopedStorage(scope);
this._list = [];
}

add(command: string): void {
this.list.unshift(command);
this.store(this.list);
this.reset();
}

next(): string {
if (this.index === null) {
this.index = 0;
} else if (this.index >= this.list.length - 1) {
this.index = this.list.length - 1;
// Bubbles.Instance.add({
// message: `We've hit the edge of the universe, nothing to see here...`,
// });
} else {
this.index = this.index + 1;
}

return this.list[this.index];
}

previous(): string | undefined {
if (this.index === null || this.index <= -1) {
this.index = -1;
// Bubbles.Instance.add({
// message: `We'll you've got to start somewhere!`,
// });
} else {
this.index = this.index - 1;
}

return this.list[this.index];
}

reset(): void {
this.index = null;
}

clear(): void {
this._list = [];
this.store(this._list);
}

store(history: string[]): void {
this.storage.set('history', JSON.stringify(history));
}

get list(): string[] {
if (!this._list) {
this._list = JSON.parse(this.storage.get('history') || '[]');
this.store(this._list);
}

return this._list;
}
}

export const History = new ScopedHistory('utils.rains.cafe');
Loading

0 comments on commit 390aee8

Please sign in to comment.