npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

termical

v0.0.20

Published

A simple reactjs component to display a terminal

Downloads

1

Readme

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

| Name | Default | Description | Type | | ------------ | ----------------- | ------------------------------------- | ------------------------------------------- | | theme | defaultTheme | Theme object | Theme | | title | Ubuntu | Terminal title | string or ReactNode | | headerHeight | 24 | Header height | number | | onClose | | Callback when close button clicked | (e) => void | | onMinimize | | Callback when minimize button clicked | (e) => void | | onZoom | | Callback when zoom button clicked | (e) => void | | prefix | > guest@ubuntu:~$ | Input prefix | string or ReactNode | | onMissing | | Callback when command not found | (args: ActionArgs) => void or Promise | | commands | | List of commands | Command[] | | children | | Intro message | ReactNode |

Root

| Name | Description | Type | | ----------- | ------------ | ----------- | | styles | Root Styles | RootStyles | | bodyProps | Body Props | BodyProps | | headerProps | Header Props | HeaderProps |

Header

| Name | Description | Type | | ---------- | ------------------------------------- | ------------------- | | styles | Header Styles | HeaderStyles | | onClose | Callback when close button clicked | (e) => void | | onMinimize | Callback when minimize button clicked | (e) => void | | onZoom | Callback when zoom button clicked | (e) => void | | h | Header height | number | | children | Terminal title | string or ReactNode |

Body

| Name | Description | Type | | --------- | ------------------------------- | ------------------------------------------- | | styles | Body Styles | BodyStyles | | commands | List of commands | Command[] | | prefix | Input prefix | string or ReactNode | | onMissing | Callback when command not found | (args: ActionArgs) => void or Promise | | topOffset | Top offset | number | | children | Intro message | ReactNode |

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 [email protected]

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();
}, []);