Skip to content

Commit

Permalink
[feat] Add the command usage syntax hint, inline
Browse files Browse the repository at this point in the history
  • Loading branch information
shubhdevelop committed Oct 6, 2024
1 parent 5484f83 commit 897187c
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 5 deletions.
26 changes: 24 additions & 2 deletions src/components/Shell/Shell.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// src/components/Shell/Shell.tsx
'use client';

import React from 'react';
// hooks
import { useShell } from './hooks/useShell';
import { SyntaxPart } from '@/data/commandSyntaxMap';

interface ShellProps {
decreaseCommandsLeft: () => void;
}

const InlineHint = ({ part }: { part: SyntaxPart }) => (
<span className="border-b border-dotted border-gray-600">
{' ' + part.syntax}
</span>
);

export default function Shell({ decreaseCommandsLeft }: ShellProps) {
const {
handleInputChange,
Expand All @@ -16,7 +24,9 @@ export default function Shell({ decreaseCommandsLeft }: ShellProps) {
inputRef,
output,
command,
remainingSyntax,
} = useShell(decreaseCommandsLeft);

return (
<div
ref={terminalRef}
Expand All @@ -34,8 +44,8 @@ export default function Shell({ decreaseCommandsLeft }: ShellProps) {
</div>
))}
<div className="flex items-center">
<p className="text-green-500 mr-2 p-1">dice ~$</p>
<div className="flex-grow">
<p className="text-green-500 mr-2 p-1 flex-shrink-0">dice ~$</p>
<div className="flex-grow relative">
<input
ref={inputRef}
type="text"
Expand All @@ -45,6 +55,18 @@ export default function Shell({ decreaseCommandsLeft }: ShellProps) {
data-testid="shell-input"
className="w-full bg-transparent outline-none text-white"
/>
<div
className="absolute top-0 left-0 text-gray-500 pointer-events-none whitespace-wrap overflow-x-auto"
style={{ paddingLeft: `${command.length + 1}ch ` }}
data-testid="inline-hint"
>
{remainingSyntax.map((part, index) => (
<React.Fragment key={index}>
{index > 0}
<InlineHint part={part} />
</React.Fragment>
))}
</div>
</div>
</div>
</div>
Expand Down
48 changes: 48 additions & 0 deletions src/components/Shell/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,52 @@ describe('Shell Component', () => {
await user.keyboard('[ArrowDown]');
expect(cliInputElement.value).toBe(newCommand);
});

it('should show syntax usage hint for SET', async () => {
const { cliInputElement, user, getByTestId } = setupTest();

const newCommand = 'set';
await user.type(cliInputElement, newCommand);

const inlineHint = getByTestId('inline-hint');
expect(inlineHint.childElementCount).toBe(4);

const inlineHintChild = inlineHint.childNodes;

expect(inlineHintChild[0]).toHaveTextContent('Key');
expect(inlineHintChild[1]).toHaveTextContent('Value');
expect(inlineHintChild[2]).toHaveTextContent('[NX | XX]');
expect(inlineHintChild[3]).toHaveTextContent(
'[EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]',
);
});

it('should show syntax usage hint for GET', async () => {
const { cliInputElement, user, getByTestId } = setupTest();

const newCommand = 'get';
await user.type(cliInputElement, newCommand);

const inlineHint = getByTestId('inline-hint');
expect(inlineHint.childElementCount).toBe(1);

const inlineHintChild = inlineHint.childNodes;

expect(inlineHintChild[0]).toHaveTextContent('Key');
});

it('should show syntax usage hint for DEL', async () => {
const { cliInputElement, user, getByTestId } = setupTest();

const newCommand = 'del';
await user.type(cliInputElement, newCommand);

const inlineHint = getByTestId('inline-hint');
expect(inlineHint.childElementCount).toBe(2);

const inlineHintChild = inlineHint.childNodes;

expect(inlineHintChild[0]).toHaveTextContent('Key');
expect(inlineHintChild[1]).toHaveTextContent('[Key ...]');
});
});
29 changes: 26 additions & 3 deletions src/components/Shell/hooks/useShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useState, useEffect, useRef, KeyboardEvent, ChangeEvent } from 'react';

// utils
import { handleCommand } from '@/shared/utils/shellUtils';
import blacklistedCommands from '@/shared/utils/blacklist';
import blacklistedCommands from '@/shared/utils/blacklist'; // Assuming you added blacklist here
import { syntaxMap, SyntaxPart } from '@/data/commandSyntaxMap';

export const useShell = (decreaseCommandsLeft: () => void) => {
// states
Expand All @@ -13,6 +14,7 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
// Initialise the command history with sessionStorage
const [commandHistory, setCommandHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState<number>(-1);
const [remainingSyntax, setRemainingSyntax] = useState<SyntaxPart[]>([]);

// useRefs
const terminalRef = useRef<HTMLDivElement>(null);
Expand All @@ -34,6 +36,24 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
decreaseCommandsLeft(); // Call to update remaining commands
};

const updateSyntax = (value: string) => {
const inputParts = value.trim().split(' ');
const command = inputParts[0].toUpperCase();
if (syntaxMap[command]) {
const parts = syntaxMap[command].parts;
if (inputParts.length === 1) {
// Only command typed, show all parts
setRemainingSyntax(parts);
} else {
// Show remaining parts based on what's already typed
const remainingParts = parts.slice(inputParts.length - 1);
setRemainingSyntax(remainingParts);
}
} else {
setRemainingSyntax([]);
}
};

useEffect(() => {
if (terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
Expand All @@ -59,9 +79,10 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
}, []);

const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setCommand(e.target.value);
// Save current input when starting to navigate history
const value = e.target.value;
setCommand(value);
setTempCommand(e.target.value);
updateSyntax(value);
};

const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
Expand All @@ -71,6 +92,7 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
setCommandHistory((prev) => [...prev, command]);
setHistoryIndex(-1);
}
setRemainingSyntax([]);
return;
}

Expand Down Expand Up @@ -120,5 +142,6 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
command,
tempCommand,
setTempCommand,
remainingSyntax,
};
};
56 changes: 56 additions & 0 deletions src/data/commandSyntaxMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export type SyntaxPart = {
syntax: string;
doc: string;
};

export type CommandSyntax = {
parts: SyntaxPart[];
};

export type SyntaxMap = {
[command: string]: CommandSyntax;
};

export const syntaxMap: SyntaxMap = {
SET: {
parts: [
{
syntax: 'Key',
doc: 'The key under which to store the value',
},
{
syntax: 'Value',
doc: 'The value to be stored',
},
{
syntax: '[NX | XX]',
doc: 'NX - Only set if key does not exist. XX - Only set if key exists',
},
{
syntax:
'[EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]',
doc: 'Options to set the key expiration: EX (seconds), PX (milliseconds), EXAT/PXAT (unix timestamp), or KEEPTTL to retain existing TTL',
},
],
},
GET: {
parts: [
{
syntax: 'Key',
doc: 'Key of the value you want to retrive',
},
],
},
DEL: {
parts: [
{
syntax: 'Key',
doc: 'Key that you want to delete',
},
{
syntax: '[Key ...]',
doc: 'Multiple keys you want to delete',
},
],
},
};

0 comments on commit 897187c

Please sign in to comment.