Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/showcase/app/(page)/terminal/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import DocComponent from '@/components/doc/DocComponent';
import features from '@/doc/terminal/features';
import { Metadata } from 'next';

export const metadata: Metadata = {
title: 'React Terminal Component',
description: 'Terminal is a text based user interface.'
};

export default function TerminalPage() {
const docs = {
features
};

return <DocComponent header="Terminal" description={metadata.description} docs={docs} apiKeys={['Terminal']} />;
}
4 changes: 4 additions & 0 deletions apps/showcase/assets/menu/menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
{
"name": "ProgressSpinner",
"to": "/progressspinner"
},
{
"name": "Terminal",
"to": "/terminal"
}
]
}
Expand Down
60 changes: 60 additions & 0 deletions apps/showcase/doc/terminal/features/basic/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Terminal } from 'primereact/terminal';
import TerminalService from 'primereact/terminalservice';
import * as React from 'react';

export default function BasicDemo() {
const commandHandler = (text: unknown): void => {
if (typeof text !== 'string') return;

let response: string | number | null;
const argsIndex: number = text.indexOf(' ');
const command: string = argsIndex !== -1 ? text.substring(0, argsIndex) : text;

switch (command) {
case 'date':
response = 'Today is ' + new Date().toDateString();
break;

case 'greet':
response = 'Hola ' + text.substring(argsIndex + 1) + '!';
break;

case 'random':
response = Math.floor(Math.random() * 100);
break;

case 'clear':
response = null;
break;

default:
response = 'Unknown command: ' + command;
break;
}

if (response) {
TerminalService.emit('response', response);
} else {
TerminalService.emit('clear');
}
};

React.useEffect(() => {
TerminalService.on('command', commandHandler);

return () => {
TerminalService.off('command', commandHandler);
};
}, []);
return (
<div className="card">
<p>
Enter &quot;<strong>date</strong>&quot; to display the current date, &quot;<strong>greet {'{0}'}</strong>&quot; for a message, &quot;<strong>random</strong>&quot; to get a random number and &quot;<strong>clear</strong>&quot; to clear
all commands.
</p>
<Terminal prompt="primereact $" aria-label="PrimeReact Terminal Service">
<Terminal.WelcomeMessage>Welcome to PrimeReact</Terminal.WelcomeMessage>
</Terminal>
</div>
);
}
19 changes: 19 additions & 0 deletions apps/showcase/doc/terminal/features/basic/doc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CodeViewer } from '@primereact/code-viewer';
import BasicDemo from './demo';
import { source } from './source.auto';

export default function BasicDoc() {
return (
<div>
<h2>Basic</h2>
<p>
Commands are processed using an EventBus implementation called <i>TerminalService</i>. Import this service into your component and subscribe to the <i>command</i> event to process the commands by sending replies with the
<i>response</i> event.
</p>

<BasicDemo />
<br />
<CodeViewer source={source} />
</div>
);
}
7 changes: 7 additions & 0 deletions apps/showcase/doc/terminal/features/basic/source.auto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/****************************************************************************
****************** PrimeReact Demo Source (Auto-Generated) ******************
*****************************************************************************/

export const source = {
"code": "import { Terminal } from 'primereact/terminal';\nimport TerminalService from 'primereact/terminalservice';\nimport * as React from 'react';\n\nexport default function BasicDemo() {\n const commandHandler = (text: unknown): void => {\n if (typeof text !== 'string') return;\n\n let response: string | number | null;\n const argsIndex: number = text.indexOf(' ');\n const command: string = argsIndex !== -1 ? text.substring(0, argsIndex) : text;\n\n switch (command) {\n case 'date':\n response = 'Today is ' + new Date().toDateString();\n break;\n\n case 'greet':\n response = 'Hola ' + text.substring(argsIndex + 1) + '!';\n break;\n\n case 'random':\n response = Math.floor(Math.random() * 100);\n break;\n\n case 'clear':\n response = null;\n break;\n\n default:\n response = 'Unknown command: ' + command;\n break;\n }\n\n if (response) {\n TerminalService.emit('response', response);\n } else {\n TerminalService.emit('clear');\n }\n };\n\n React.useEffect(() => {\n TerminalService.on('command', commandHandler);\n\n return () => {\n TerminalService.off('command', commandHandler);\n };\n }, []);\n return (\n <div className=\"card\">\n <p>\n Enter &quot;<strong>date</strong>&quot; to display the current date, &quot;<strong>greet {'{0}'}</strong>&quot; for a message, &quot;<strong>random</strong>&quot; to get a random number and &quot;<strong>clear</strong>&quot; to clear\n all commands.\n </p>\n <Terminal prompt=\"primereact $\" aria-label=\"PrimeReact Terminal Service\">\n <Terminal.WelcomeMessage>Welcome to PrimeReact</Terminal.WelcomeMessage>\n </Terminal>\n </div>\n );\n}\n"
};
5 changes: 5 additions & 0 deletions apps/showcase/doc/terminal/features/import/doc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CodeViewer } from '@primereact/code-viewer';

export default function ImportDoc() {
return <CodeViewer source={`import { Terminal } from 'primereact/terminal';`} />;
}
18 changes: 18 additions & 0 deletions apps/showcase/doc/terminal/features/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';
import BasicDoc from './basic/doc';
import ImportDoc from './import/doc';

const features = [
{
id: 'import',
label: 'Import',
component: ImportDoc
},
{
id: 'basic',
label: 'Basic',
component: BasicDoc
}
];

export default features;
2 changes: 2 additions & 0 deletions packages/headless/src/terminal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useTerminal';
export * from './useTerminal.props';
5 changes: 5 additions & 0 deletions packages/headless/src/terminal/useTerminal.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { useTerminalProps } from '@primereact/types/shared/terminal';

export const defaultProps: useTerminalProps = {
__TYPE: 'useTerminal'
};
Empty file.
107 changes: 107 additions & 0 deletions packages/headless/src/terminal/useTerminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { withHeadless } from '@primereact/core/headless';
import { CommandItem } from '@primereact/types/shared/terminal';
import { focus } from '@primeuix/utils/dom';
import TerminalService from 'primereact/terminalservice';
import * as React from 'react';
import { defaultProps } from './useTerminal.props';

export const useTerminal = withHeadless({
setup: ({ elementRef }) => {
const [commandTextState, setCommandTextState] = React.useState<string>('');
const [commandsState, setCommandsState] = React.useState<CommandItem[]>([]);
const [indexState, setIndexState] = React.useState<number>(0);
const [emittedTextState, setEmittedTextState] = React.useState<string>('');
const inputRef = React.useRef<HTMLInputElement>(null);
const isEmitted = React.useRef<boolean>(false);

React.useEffect(() => {
const response = (res: unknown) => {
if (commandsState && commandsState.length > 0) {
const commands = [...commandsState];

commands[commands.length - 1].response = String(res);

setCommandsState(commands);
}
};

const clear = () => {
setCommandsState([]);
setIndexState(0);
};

TerminalService.on('response', response);
TerminalService.on('clear', clear);

return () => {
TerminalService.off('response', response);
TerminalService.off('clear', clear);
};
}, [commandsState]);

React.useEffect(() => {
if (isEmitted.current) {
TerminalService.emit('command', emittedTextState);
isEmitted.current = false;
}

if (elementRef && 'current' in elementRef && elementRef.current) {
elementRef.current.scrollTop = elementRef.current.scrollHeight;
}
});

const onClick = () => {
if (inputRef.current) {
focus(inputRef.current);
}
};

const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
switch (event.code) {
case 'ArrowUp':
if (commandsState && commandsState.length) {
const prevIndex = indexState - 1 < 0 ? commandsState.length - 1 : indexState - 1;
const command = commandsState[prevIndex];

setIndexState(prevIndex);
setCommandTextState(command.text);
}

break;

case 'Enter':
case 'NumpadEnter':
if (commandTextState) {
const newCommands = [...commandsState];

newCommands.push({ text: commandTextState });

setIndexState((prevIndex) => prevIndex + 1);
setCommandTextState('');
setCommandsState(newCommands);
setEmittedTextState(commandTextState);
isEmitted.current = true;
}

break;

default:
break;
}
};

const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCommandTextState(e.target.value);
};

return {
commandsState,
commandTextState,
inputRef,
onClick,
onKeyDown,
onInputChange
};
},
defaultProps
});
8 changes: 8 additions & 0 deletions packages/primereact/src/terminal/Terminal.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as HeadlessTerminal from '@primereact/headless/terminal';
import type { TerminalProps } from '@primereact/types/shared/terminal';

export const defaultProps: TerminalProps = {
...HeadlessTerminal.defaultProps,
__TYPE: 'Terminal',
prompt: undefined
};
Empty file.
Loading
Loading