0.0.20 • Published 1 year ago

termical v0.0.20

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

Terminal Component ⬛ Termical

This is not an advanced terminal, it's just a simple terminal component that can be used in any React app, for more advanced terminal, check out xterm.js

  • ✅ Typescript
  • ✅ Fully customizable
  • ✅ Persisted history
  • ⚙️ Auto complete
  • 🎨 Themable
  • 😞 ReactJS only

Install

npm install termical
pnpm add termical
yarn add termical

Glossary

  • Line - A line in the terminal.
  • Stack - An executed command history stack.

Usage

import { Terminal } from "termical";

const Demo = () => {
  return (
    <div style={{ height: 400 }}>
      <Terminal {...props} />
    </div>
  );
};

// OR
import { Root, Header, Body } from "termical";

const Demo = () => {
  const HEADER_HEIGHT = 24;
  return (
    <div style={{ height: 400 }}>
      <Root {...rootProps}>
        <Header {...headerProps} h={HEADER_HEIGHT} />
        <Body {...bodyProps} topOffset={HEADER_HEIGHT} />
      </Root>
    </div>
  );
};

Add Commands

import { Terminal } from "termical";

const Demo = () => {
  const commands = [
    {
      text: "hello",
      description: "Say Hello!!",
      action({ value, ctx }) {
        ctx.line.add({
          content: `Hey 👋👋👋`,
          id: Math.random().toString(),
          timestamp: new Date(),
        });
      },
    },
  ];
  return <Terminal commands={commands} />;
};

useTerminal

import { useTerminal } from "termical"

const Demo = () => {
  const { line, stack } = useTerminal();

  line.add(...);
  line.addMany(...);
  line.update(...);
  line.removeMany(...);
  line.reset(...);
  line.lines;

  // same for stack

  return <div>...</div>;
};

Default Commands

  • help - Show all commands
  • history - Show command history
  • clear - Clear the terminal
  • clear history - Clear command history
  • Note: You can override a default command by adding a command with the same text property

Theming

import { Terminal } from "termical";

const Demo = () => {
  const theme = {
    header: {
      text: {
        color: "#facc15",
      },
      container: {
        backgroundColor: "#14532d",
      },
    },
    body: {
      container: {
        backgroundColor: "#15803d",
        color: "#fde047",
      },
      scrollbar: {
        thumb: {
          backgroundColor: "#ca8a04",
        },
        track: {
          background: "#15803d",
        },
      },
      scrollArea: {
        paddingLeft: 12,
        paddingRight: 12,
      },
    },
  };
  return <Terminal theme={theme} />;
};

Props

Terminal

NameDefaultDescriptionType
themedefaultThemeTheme objectTheme
titleUbuntuTerminal titlestring or ReactNode
headerHeight24Header heightnumber
onCloseCallback when close button clicked(e) => void
onMinimizeCallback when minimize button clicked(e) => void
onZoomCallback when zoom button clicked(e) => void
prefix> guest@ubuntu:~$Input prefixstring or ReactNode
onMissingCallback when command not found(args: ActionArgs) => void or Promise
commandsList of commandsCommand[]
childrenIntro messageReactNode

Root

NameDescriptionType
stylesRoot StylesRootStyles
bodyPropsBody PropsBodyProps
headerPropsHeader PropsHeaderProps

Header

NameDescriptionType
stylesHeader StylesHeaderStyles
onCloseCallback when close button clicked(e) => void
onMinimizeCallback when minimize button clicked(e) => void
onZoomCallback when zoom button clicked(e) => void
hHeader heightnumber
childrenTerminal titlestring or ReactNode

Body

NameDescriptionType
stylesBody StylesBodyStyles
commandsList of commandsCommand[]
prefixInput prefixstring or ReactNode
onMissingCallback when command not found(args: ActionArgs) => void or Promise
topOffsetTop offsetnumber
childrenIntro messageReactNode

Interface

type Theme = {
  header?: {
    text?: CSSObject;
    container?: CSSObject;
  };
  body?: {
    container?: CSSObject;
    scrollbar?: {
      thumb?: CSSObject;
      track?: CSSObject;
    };
    scrollArea?: CSSObject;
  };
};

type Line = {
  id: string;
  content: string | React.ReactNode;
  timestamp: Date;
};
type Stack = {
  text: string;
  timestamp: Date;
};
type Command = {
  text: string;
  action: (args: ActionArgs) => void | Promise<void>;
  exact?: boolean;
  description?: string;
};

type Context = {
  commands: Command[];
  lines: Line[];
  stacks: Stack[];
  line: LinesState;
  stack: StacksState;
};

type ActionArgs = {
  value: string;
  event: React.KeyboardEvent<HTMLTextAreaElement>;
  ctx: Context;
};

Styles API Interface

type RootStyles = {
  container?: CSSObject;
};
type HeaderStyles = {
  root?: CSSObject;
  title?: CSSObject;
  close?: CSSObject;
  minimize?: CSSObject;
  zoom?: CSSObject;
};

type BodyStyles = {
  container?: CSSObject;
  scrollArea?: CSSObject;
  input?: CSSObject;
  node?: CSSObject;
};

Examples

Fullscreen terminal

const Demo = () => {
  // without container height, the terminal will be fullscreen
  return <Terminal />;
};

// OR
const Demo = () => {
  const HEADER_HEIGHT = 24;
  return (
    <div
      style={{
        position: "absolute",
        left: 0,
        right: 0,
        bottom: 0,
        top: 0,
      }}
    >
      <Root>
        <Header h={HEADER_HEIGHT} />
        <Body topOffset={HEADER_HEIGHT} />
      </Root>
    </div>
  );
};

Add intro message

const Demo = () => {
  return (
    <Terminal>
      <p>Hello 👋!!</p>
    </Terminal>
  );
};

// OR
const Demo = () => {
  const HEADER_HEIGHT = 24;
  return (
    <Root>
      <Header h={HEADER_HEIGHT} />
      <Body topOffset={HEADER_HEIGHT}>
        <p>Hello 👋!!</p>
      </Body>
    </Root>
  );
};

Clearable intro message

Note: the intro message will get added twice in development mode if you're on react@18.x.x

const { line } = useTerminal();

useEffect(() => {
  let intro = line.lines.find((line) => line.id === "intro");

  if (!intro) {
    line.add({
      id: "intro",
      content: (
        <div>
          <p>
            😌 It will be nice to execute your commands here. <br />
            <br />
            Type <em>help</em> to see available commands.
          </p>
        </div>
      ),
      timestamp: new Date(),
    });
  }
}, []);

// 😌 It will be nice to execute your commands here.
//
//
// Type _help_ to see available commands.
// > :~$

The power of line.update()

In this example we used line.update() from useTerminal to create a a hook that will update a line every delay milliseconds.

  • it will create a line with id flashy_message if it doesn't exist
  • it will update the line with id flashy_message with the next number
  • update will be called every delay milliseconds
import { useTerminal } from "termical";

const useFlashyMessage = (delay: number | null) => {
  const id = "flashy_message";

  const { line } = useTerminal();

  // https://usehooks-ts.com/react-hook/use-interval
  useInterval(() => {
    let inLine = line.lines.find((line) => line.id === id)?.content;
    if (!inLine) {
      line.add({
        id,
        content: " ",
        timestamp: new Date(),
      });

      inLine = line.lines.find((line) => line.id === id)?.content;
    }
    const message = (Number(inLine) + 1).toString();

    line.update(id, {
      content: isNaN(Number(message)) ? "0" : message,
    });
  }, delay);
};

// Usage
const Demo = () => {
  const [delay, setDelay] = useState(null);
  // we set `delay` to null to stop the interval until we
  // decide to start it

  useFlashyMessage(delay);

  return (
    <>
      <Terminal>
        <p>Hello 👋!!</p>
      </Terminal>
      <button onClick={() => setDelay(1000)}>Start</button>
      <button onClick={() => setDelay(null)}>Stop</button>
    </>
  );
};

command with --options

To handle commands with options, we can use exact: false and parse the value.

const commands = [
  ...,
  {
    text: "hello",
    description: "Say Hello!!",
    exact: false,
    action({ value, ctx }) {
      const _value = value.replace("hello ", "✨")

      ctx.line.add({
        content: `Hey ${_value}✨!`,
        id: Math.random().toString(),
        timestamp: new Date(),
      });
    },
  }
  ...,
]

// :~$ hello world
// Hey ✨world✨!

Executing another command

const commands = [
  {
    text: "hey",
    description: "Mirror 'hello' command",
    action(args) {
      args.ctx.commands
        .find((command) => command.text === "hello")
        ?.action(args);
    },
  },
];

Use as a log viewer

We will create a custom hook to make life easier.

const useLog = () => {
  const { line } = useTerminal();

  return {
    log: (content: string | React.ReactNode) => {
      line.add({
        id: Math.random().toString(),
        content,
        timestamp: new Date(),
      });
    },
    Terminal: () => (
      <Root>
        <Body topOffset={0} prefix="" />
      </Root>
    ),
  };
};

const Demo = () => {
  const { log, Terminal } = useLog();
  log("Hello World!");

  return (
    <div>
      <Terminal />

      <button onClick={() => log("👋👋 Hello World!")}>Say Hello</button>
    </div>
  );
};

// result
// Hello World!
// 👋👋 Hello World! <--- after clicking the button
// 👋👋 Hello World! <--- after clicking the button again

No persistent history

const { line, stack } = useTerminal();

useEffect(() => {
  line.reset();

  // also reset the history
  // stack.reset();
}, []);