Added commands to terminal
This commit is contained in:
parent
f39e663cd7
commit
f8efae6f22
9 changed files with 244 additions and 9 deletions
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"editor.rulers": [
|
||||||
|
120
|
||||||
|
],
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"editorRuler.foreground": "#1F1F1F"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.Window-container {
|
.Window-container {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
background-color: var(--background-color-c);
|
background-color: var(--background-color-c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,9 +32,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.Window-icon {
|
.Window-icon {
|
||||||
|
height: 1rem;
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Window-icon > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.Header > p {
|
.Header > p {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -59,5 +68,5 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.Window-content {
|
.Window-content {
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -7,10 +7,11 @@ import { useWindowsManager } from "../hooks/WindowsManagerContext.js";
|
||||||
import Draggable from "react-draggable";
|
import Draggable from "react-draggable";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
export function Window({ id, app, size, position, focused = false, minimized = false }) {
|
export function Window({ id, app, size, position, focused = false }) {
|
||||||
const windowsManager = useWindowsManager();
|
const windowsManager = useWindowsManager();
|
||||||
const nodeRef = useRef(null);
|
const nodeRef = useRef(null);
|
||||||
const [maximized, setMaximized] = useState(false);
|
const [maximized, setMaximized] = useState(false);
|
||||||
|
const [minimized, setMinimized] = useState(false);
|
||||||
|
|
||||||
const [screenWidth, setScreenWidth] = useState(100);
|
const [screenWidth, setScreenWidth] = useState(100);
|
||||||
const [screenHeight, setScreenHeight] = useState(100);
|
const [screenHeight, setScreenHeight] = useState(100);
|
||||||
|
|
@ -24,6 +25,12 @@ export function Window({ id, app, size, position, focused = false, minimized = f
|
||||||
resizeObserver.observe(document.getElementById("root"));
|
resizeObserver.observe(document.getElementById("root"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const classNames = ["Window-container"];
|
||||||
|
if (maximized)
|
||||||
|
classNames.push("Maximized");
|
||||||
|
if (minimized)
|
||||||
|
classNames.push("Minimized");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable
|
<Draggable
|
||||||
axis="both"
|
axis="both"
|
||||||
|
|
@ -42,7 +49,7 @@ export function Window({ id, app, size, position, focused = false, minimized = f
|
||||||
disabled={maximized}
|
disabled={maximized}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`Window-container ${maximized ? "Maximized" : ""}`}
|
className={classNames.join(" ")}
|
||||||
ref={nodeRef}
|
ref={nodeRef}
|
||||||
style={{
|
style={{
|
||||||
width: maximized ? screenWidth : size.x,
|
width: maximized ? screenWidth : size.x,
|
||||||
|
|
@ -52,7 +59,7 @@ export function Window({ id, app, size, position, focused = false, minimized = f
|
||||||
<div className="Header">
|
<div className="Header">
|
||||||
<ReactSVG className="Window-icon" src={process.env.PUBLIC_URL + `/media/applications/icons/${app.id}.svg`}/>
|
<ReactSVG className="Window-icon" src={process.env.PUBLIC_URL + `/media/applications/icons/${app.id}.svg`}/>
|
||||||
<p>{app.name}</p>
|
<p>{app.name}</p>
|
||||||
<button>
|
<button onClick={() => setMinimized(!minimized)}>
|
||||||
<FontAwesomeIcon icon={faMinus}/>
|
<FontAwesomeIcon icon={faMinus}/>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setMaximized(!maximized)}>
|
<button onClick={() => setMaximized(!maximized)}>
|
||||||
|
|
@ -63,7 +70,7 @@ export function Window({ id, app, size, position, focused = false, minimized = f
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="Window-content">
|
<div className="Window-content">
|
||||||
|
{app.windowContent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
|
|
||||||
127
src/components/applications/terminal/Terminal.js
Normal file
127
src/components/applications/terminal/Terminal.js
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import styles from "./Terminal.module.css";
|
||||||
|
import { Command } from "./commands.js";
|
||||||
|
|
||||||
|
const PREFIX = "$ ";
|
||||||
|
|
||||||
|
function OutputLine({ text }) {
|
||||||
|
return (
|
||||||
|
<p className={styles.Output}>{text}</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputLine({ value, prefix, onChange, onKeyUp, onKeyDown }) {
|
||||||
|
return (
|
||||||
|
<span className={styles.Input}>
|
||||||
|
{prefix && <p className={[styles.Prefix]}>{prefix}</p>}
|
||||||
|
<input
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
spellCheck={false}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Terminal() {
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
const [history, setHistory] = useState([]);
|
||||||
|
|
||||||
|
const updatedHistory = history;
|
||||||
|
const pushHistory = (entry) => {
|
||||||
|
updatedHistory.push(entry);
|
||||||
|
setHistory(updatedHistory);
|
||||||
|
};
|
||||||
|
|
||||||
|
const promptOutput = (text) => {
|
||||||
|
pushHistory({
|
||||||
|
text,
|
||||||
|
isInput: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitInput = (value) => {
|
||||||
|
// To do: make empty submit go to new line
|
||||||
|
if (value.trim() === "")
|
||||||
|
return;
|
||||||
|
|
||||||
|
pushHistory({
|
||||||
|
text: PREFIX + value,
|
||||||
|
isInput: true
|
||||||
|
});
|
||||||
|
|
||||||
|
setInputValue("");
|
||||||
|
|
||||||
|
value = value.trim();
|
||||||
|
|
||||||
|
const args = value.split(/ +/);
|
||||||
|
const commandName = args.shift().toLowerCase();
|
||||||
|
|
||||||
|
const command = Command.find(commandName);
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
return promptOutput("Command not found: " + commandName);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = command.execute(args, {
|
||||||
|
promptOutput,
|
||||||
|
pushHistory
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
|
return promptOutput("Command failed.");
|
||||||
|
|
||||||
|
if (!response.blank)
|
||||||
|
promptOutput(response);
|
||||||
|
} catch (error) {
|
||||||
|
promptOutput("Command failed.");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
|
||||||
|
// console.log(event);
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
submitInput(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
return setInputValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayHistory = () => {
|
||||||
|
const visibleHistory = history.slice(-16);
|
||||||
|
let startIndex = 0;
|
||||||
|
|
||||||
|
visibleHistory.forEach((entry, index) => {
|
||||||
|
if (entry.clear)
|
||||||
|
startIndex = index + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return visibleHistory.slice(startIndex).map(({ text }, index) => {
|
||||||
|
return <OutputLine text={text} key={index}/>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.Terminal}>
|
||||||
|
{displayHistory()}
|
||||||
|
<InputLine
|
||||||
|
value={inputValue}
|
||||||
|
prefix={PREFIX}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
34
src/components/applications/terminal/Terminal.module.css
Normal file
34
src/components/applications/terminal/Terminal.module.css
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
.Terminal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Terminal p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Prefix {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Input, .Output {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 1.25rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Input input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: inherit;
|
||||||
|
margin-left: 0.15rem;
|
||||||
|
}
|
||||||
42
src/components/applications/terminal/commands.js
Normal file
42
src/components/applications/terminal/commands.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
export class Command {
|
||||||
|
/**
|
||||||
|
* @param {String} name
|
||||||
|
* @param {Function} execute
|
||||||
|
*/
|
||||||
|
constructor(name, execute) {
|
||||||
|
this.name = name;
|
||||||
|
this.execute = execute;
|
||||||
|
this.aliases = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addAlias(alias) {
|
||||||
|
this.aliases.push(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
static find(name) {
|
||||||
|
let matchCommand = null;
|
||||||
|
|
||||||
|
this.COMMANDS.forEach((command) => {
|
||||||
|
if (command.name === name) {
|
||||||
|
matchCommand = command;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
static COMMANDS = [
|
||||||
|
new Command("echo", (args) => {
|
||||||
|
return args.join(" ");
|
||||||
|
}),
|
||||||
|
new Command("clear", (args, { pushHistory }) => {
|
||||||
|
pushHistory({
|
||||||
|
clear: true,
|
||||||
|
isInput: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return { blank: true };
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
/* eslint-disable eqeqeq */
|
/* eslint-disable eqeqeq */
|
||||||
|
import { Terminal } from "../../components/applications/terminal/Terminal.js";
|
||||||
import Application from "./application.js";
|
import Application from "./application.js";
|
||||||
|
|
||||||
export default class ApplicationsManager {
|
export default class ApplicationsManager {
|
||||||
static APPLICATIONS = [
|
static APPLICATIONS = [
|
||||||
new Application("Terminal", "terminal"),
|
new Application("Terminal", "terminal", <Terminal/>),
|
||||||
|
// new Application("Browser", "browser"),
|
||||||
|
new Application("Code Editor", "code-editor"),
|
||||||
|
new Application("File Explorer", "file-explorer"),
|
||||||
|
new Application("Media Viewer", "media-viewer"),
|
||||||
]
|
]
|
||||||
|
|
||||||
static getApplication(id) {
|
static getApplication(id) {
|
||||||
|
|
|
||||||
3
src/modules/math/random.js
Normal file
3
src/modules/math/random.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function randomRange(min, max) {
|
||||||
|
return Math.random() * (max - min) + min;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import ApplicationsManager from "../applications/applications.js";
|
import ApplicationsManager from "../applications/applications.js";
|
||||||
|
import { randomRange } from "../math/random.js";
|
||||||
import Vector2 from "../math/vector2.js";
|
import Vector2 from "../math/vector2.js";
|
||||||
|
|
||||||
export default class WindowsManager {
|
export default class WindowsManager {
|
||||||
|
|
@ -10,8 +11,8 @@ export default class WindowsManager {
|
||||||
|
|
||||||
open(appId) {
|
open(appId) {
|
||||||
const app = ApplicationsManager.getApplication(appId);
|
const app = ApplicationsManager.getApplication(appId);
|
||||||
const size = new Vector2(800, 400);
|
const size = new Vector2(700, 400);
|
||||||
const position = new Vector2(300, 200);
|
const position = new Vector2(randomRange(50, 600), randomRange(50, 450));
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
while (this.windowIds.includes(id.toString())) {
|
while (this.windowIds.includes(id.toString())) {
|
||||||
|
|
@ -28,7 +29,6 @@ export default class WindowsManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateWindows(this.windows);
|
this.updateWindows(this.windows);
|
||||||
|
|
||||||
// console.log(this);
|
// console.log(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue