#css
    #react
    #linux

Creating an Ubuntu Terminal

The Ubuntu Terminal

If you've used Ubuntu ā€” or even heard of it before ā€” you might be familiar with the iconic and well-known Ubuntu Terminal.

It works very similar to the Windows command prompt, or MacOS terminal.

It's a joy of an interface and simplistic style makes it incredibly easy to use. You can learn how to create your own Ubuntu Terminal using nothing more than React and CSS.

Once done, it'll looking something like this Ubuntu Terminal GIF


Getting Started

We'll be using a platform called StackBlitz to write and run our code.

It's quicker and easier to start by editing this starter project here which has all the dependencies and folder structure already included Ubuntu Terminal.

Feel free to skip to the next section, unless your developing in your own editor.

If you want to develop with your own editor, you'll just have to create a new React project and ensure you have a similar folder structure to the starter project above.

You'll also need add the following dependencies yourself using npm install.

  • @fortawesome/fontawesome-svg-core
  • @fortawesome/free-regular-svg-icons
  • @fortawesome/free-solid-svg-icons
  • @fortawesome/react-fontawesome
  • node-sass

The Application Component

Let's start by modifying our main application over at index.tsx. We'll add an object called config that'll hold our name and our computer's name.

const config = {
  name: 'Me',
  computerName: 'Ubuntu'
};

Add it to your application component. After that, your index.tsx file should look something like this

import React, { Component } from 'react';
import { render } from 'react-dom';
import './styles.scss';

const Application = () => {

  const config = {
    name: 'Me',
    computerName: 'Ubuntu'
  };
	
  return (
    <div className="application">
    </div>
  )
}

render(<Application />, document.getElementById('root'));

Next, we'll want to add some basic styling for the component. We want our application to take up the whole screen and to use the background from the repository ubuntu-background.png.

In addition to that, we want it to center whatever is in the middle of the screen ā€” our Ubuntu Terminal, eventually, so we'll do that using Flexbox. Let's modify our styles.scss to look like this

body {
  margin: 0;
}

.application {
  width: 100vw;
  height: 100vh;

  background-image: url("https://cdn.jsdelivr.net/gh/cjativa/ubuntu-tutorial@Complete/ubuntu-background.png");

  display: flex;
  justify-content: center;
  align-items: center;
} 

Our Ubuntu Terminal ā€” Navigation Bar

Next up is the Navigation Bar component. We'll be modifying the file navigationBar.tsx inside of the UbuntuTerminal folder.

We`ll want to import the modules we need into the component, so let's throw these at the top of the file

import React, { Component } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSquare } from "@fortawesome/free-regular-svg-icons";
import {
  faSearch,
  faBars,
  faMinus,
  faTimes,
  faPlus,
  faCircle
} from "@fortawesome/free-solid-svg-icons";

Nothing too crazy here ā€” just importing the usual React modules and some icons we'll use for the navigation bar. As you can tell from the Ubuntu Terminal in the beginning of the tutorial, we need a search icon, a stacked bars icon, a plus icon, a square icon, etc ā€” which we're getting from FontAwesome.

Eventually, our navigation bar component will receive the config props from somewhere. We'll pull the name and computerName variables from the config to show in our navigation. For that, the component should look like this

export const NavigationBar = props => {

  // Get our config from the props
  const { config } = props;

  // Extract the name and computer name
  const { name, computerName } = config;

  return (
    <div className="navigation-bar">
    </div>
  );
};

Now we've got to create the three different parts of the navigation bar ā€” the left, the center, and right. These will go in between the two div elements of our navigation bar.

<div className="navigation-bar">
  ... // The left, center
  ... // and right side code will go in here
</div>

The left will be made from this ā€” which includes the plus icon.

{/** Left side of the navigation bar */}
  <div className="nb__left">
    <FontAwesomeIcon icon={faPlus} size="sm" className="raised" />
  </div>

The center is next up ā€” which includes just our name and computer name. We use string-interpolation to display it in the {${name}@${computerName}:~} line.

{/** Center of the navigation bar */}
<div className="nb__center">
  <span className="nb__center--cp">{`${name}@${computerName}:~`}</span>
</div>

The right side is last ā€” which includes the most work. It will display the search icon, the stacked bars, the minus sign, etc.

{/** Right side of the navigation bar */}
<div className="nb__right">
  <FontAwesomeIcon icon={faSearch} size="sm" className="raised" />
  <FontAwesomeIcon icon={faBars} size="sm" className="raised" />
  <FontAwesomeIcon icon={faMinus} size="sm" className="min" />
  <FontAwesomeIcon icon={faSquare} size="sm" className="max-square" />

{/** Stacked FontAwesome icons for the "Close" button */}
  <span className="fa-stack custom">
    <FontAwesomeIcon icon={faCircle} size="xs" className="fa-stack-2x close"  />
    <FontAwesomeIcon icon={faTimes} size="xs" className="fa-stack-1x" />
  </span>
</div>

Last but not least, some styling. Let's create a new styles folder at the root of our repository and we'll add a navigationBar.scss file in there.

We'll add the following code to make our navigation bar look like the Ubuntu one

.navigation-bar {
	height: 35px;
	background-color: #312f30;
	color: #ffffff;
	padding: 0px 10px;

	display: grid;
	grid-template-columns: repeat(3, 1fr);
	justify-content: center;
	align-items: center;

	position: relative;

	.nb {
		&__center {
			justify-self: center;
			font-weight: 300;
			cursor: default;
			user-select: none;
		}

		&__right {
			justify-self: end;

			display: grid;
			grid-template-columns: repeat(4, 1fr) min-content;
			column-gap: 10px;

			justify-items: center;
			align-items: center;
		}
	}

	.custom {
		height: 1em;
	}

	.close {
		color: #312f30;
	}

	.custom:hover .close {
		color: #e25728;
	}

	.raised {
		background-color: #403e3f;
		border: 1px solid #282626;
		border-radius: 4px;
		padding: 5px 8px;
	}

	.max-square,
	.min-square,
	.min {
		padding: 5px 8px;
		&:hover {
			background-color: #403e3f;
		}
	}
}

After that, make sure you modify your styles.scss file to include this line @import './styles/navigationBar.scss';


Accepting Input from the User

We've got a good amount of the body of the terminal going. We have state variables for entering input as well as storing entered inputs.

We also have event listeners that will submit input upon the enter key being pressed. Let's add some code to collect input

/** Handles input changes in the terminal input line */
  const onInputChange = event => {
    const value = event.target.value;
    setInput(value);
  };

/** Scrolls our terminal to the bottom as more input is entered */
  useEffect(() => {
    const updateScrollToBottom = () => {
      const terminalContent = terminalContainerRef.current;
      terminalContent.scrollTo({
        top: terminalContent.scrollHeight,
        behavior: "smooth"
      });
    };

    updateScrollToBottom();
  }, [inputs]);

In the above, we collect input every time input is entered in the terminal line and update the state using setInput().

We have a useEffect handler for when more inputs are entered to scroll our terminal to the bottom.


Displaying Previously Entered Input

Now that we can collect input, we'll want to display the input a user have previously entered along with that cool line you see at the beginning of every line.

We'll have a function that generates the Me@Ubuntu:~$ line and a function that generate the previously entered input lines.

 /** Generates the `Me@Ubuntu:~$`  */
 const startOfLine = () => {
   const { name, computer } = config;

    return (
      <span className="command__green" style={{ marginRight: "5px" }}>
        {`${name}@${computer}`}
        <span className="command__white">:</span>
        <span className="command__blue">~</span>
        <span className="command__white">$</span>
      </span>
    );
  };

  /** Generates the lines of previous input */
  const generatePreviousInputLines = () => {
    return (
      <div>
        {/** Renders the list of previously entered inputs */}
        {inputs.map((input, index) => {
          return (
            <div key={index}>
              <div>
                {startOfLine()} {input}
              </div>
            </div>
          );
        })}
      </div>
    );
};

Finishing up the Body of the Terminal

We can accept input in the terminal and we can display previously entered input.

However, we have to add the final code for actually rendering those. We'll update the bottom of our Body component to look like this

return (
    <div
      className="body"
      onClick={() => inputRef.current.focus()}
      style={{ overflowY: "auto", height: "100%", whiteSpace: "pre-line" }}
      ref={terminalContainerRef}
    >
      {/** Renders the previous input */}
      {generatePreviousInputLines()}

      {/** Renders the command input line  */}
      <div className="input-container">
        {startOfLine()}
        <input
          className="terminal-input"
          type="text"
          autoFocus
          onChange={onInputChange}
          spellCheck={false}
          value={input}
          ref={inputRef}
        />
      </div>
    </div>
  );

You can see we have the line

{/** Renders the previous input */}
{generatePreviousInputLines()}

which will just render our previous inputs we've entered. We also have

{/** Renders the previous input */}
{startOfLine()}

which will render our cool string at the beginning of every input we will enter.

Let's add some quick styling to the body component. Create a body.scss in the styles folder. We'll add this code for styling it

.body {
	width: 100%;
	height: 100%;
	padding: 3px 5px;

	cursor: default;

	background: #300a24;
	color: #ffffff;

	position: relative;
	overflow: hidden;
}

.command {
	&__green {
		color: #8be331;
	}
	&__white {
		color: #ffffff;
	}

	&__blue {
		color: #739ece;
	}
}

.input-container {
	display: flex;
	flex-direction: row;
}

.terminal-input {
  border: none;

  color: #ffffff;
  background: none;

  font-family: 'Ubuntu Mono', monospace;;
  font-size: 16px;

  &:active, &:focus {
    outline: none;
  }
}

then add this line@import './styles/body.scss'; to your styles.sss to bring in the styles.


Putting the Ubuntu Terminal Together

Let's see all the hurdles we've crossed so far

āœ”ļø We've created and styled our NavigationBar component
āœ”ļø We've created and styled our Body component
āœ”ļø We're able to accept input and display previously entered input

These aren't being displayed yet but that's where we'll work on now and it'll only be a few lines.

Open up your ubuntuTerminal.tsx file and we'll add the following to it

import React, { Component } from "react";

import { NavigationBar } from "./navigationBar";
import { Body } from "./body";

In the above we're just importing the usual React modules ā€” but we're also importing our <NavigationBar /> and <Body /> components so we can display them.

Let's construct the frame our of Ubuntu Terminal component

/** The Ubuntu Terminal component */
export const UbuntuTerminal = props => {
  const { appRef, config } = props;

  return (
    <div className="ubuntu">
      <NavigationBar config={config} />
      <Body config={config} />
    </div>
  );
};

We'll also want to add some minor styling to our Ubuntu Terminal component. Let's create a new file ubuntuTerminal.scss in the styles folder and add this code to it

.ubuntu {
  height: 300px;
  width: 600px;
  padding: 0px;
  
  overflow: hidden;
  border-radius: 10px;
  
  font-family: 'Ubuntu Mono', monospace;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
  position: absolute;

  display: grid;
  grid-template-rows: 35px 1fr;
}

And then import it to our styles.scss the same way we've done all of the others ā€” by adding this to the file @import './styles/ubuntuTerminal.scss';.


Completing Our Ubuntu Terminal

The final piece of the puzzle is to bring in our <UbuntuTerminal /> component into our application component for rendering. Once that's done we'll be all set and our terminal will finally be complete.

Open up your index.tsx and ensure the return (...) part of the application component looks like this

...
return (
    <div className="application">
      <UbuntuTerminal config={config} />
    </div>
);
...

Here, we're telling our application component to render our <UbuntuTerminal /> . In addition, we're passing the config variable as a prop to the Ubuntu Terminal component so it can use it.

Feel free to modify that configuration to whatever name or computer name you'd like. It will look like this

Completed Ubuntu Terminal

And there you have it, your own styled Ubuntu Terminal!

See the completed code here Ubuntu Terminal by switching to the "Complete" branch in the top-left.


zerochass

practical and goal-oriented resources

learn by doing. enjoy what you do.