Updated terminal app
This commit is contained in:
parent
23062bcd29
commit
c4868a040f
38 changed files with 688 additions and 184 deletions
|
|
@ -11,7 +11,7 @@ import { ModalsView } from "../../modals/ModalsView.jsx";
|
|||
import { QuickAccessButton } from "./QuickAccessButton.jsx";
|
||||
import { useDialogBox } from "../../../hooks/modals/dialogBox.js";
|
||||
import Vector2 from "../../../features/math/vector2.js";
|
||||
import { DIALOG_CONTENT_TYPES } from "../../modals/dialog-box/DialogBox.jsx";
|
||||
import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js";
|
||||
|
||||
/**
|
||||
* @param {object} props
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import { SettingsManager } from "../../../features/settings/settingsManager.js";
|
|||
import styles from "./Settings.module.css";
|
||||
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
|
||||
import { useSettingsManager } from "../../../hooks/settings/settingsManagerContext.js";
|
||||
|
||||
const wallpapersPath = "~/Images/Wallpapers";
|
||||
import { WALLPAPERS_PATH } from "../../../constants/applications/settings.js";
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const virtualRoot = useVirtualRoot();
|
||||
|
|
@ -25,7 +24,7 @@ export function AppearanceSettings() {
|
|||
<div className={styles["Option"]}>
|
||||
<p className={styles["Label"]}>Wallpaper</p>
|
||||
<div className={styles["Input"]}>
|
||||
{virtualRoot.navigate(wallpapersPath)?.getFiles()?.toReversed().map(({ id, source }) =>
|
||||
{virtualRoot.navigate(WALLPAPERS_PATH)?.getFiles()?.toReversed().map(({ id, source }) =>
|
||||
<label className={styles["Image-select"]} key={id}>
|
||||
<input
|
||||
type="radio"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export function OutputLine({ text }) {
|
|||
|
||||
return (<>
|
||||
{lines.map((line, index) =>
|
||||
<p key={index} className={styles.Output}>{line}</p>
|
||||
<pre key={index} className={styles.Output}>{line === "" ? " " : line}</pre>
|
||||
)}
|
||||
</>);
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import styles from "./Terminal.module.css";
|
||||
import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.js";
|
||||
import { Command } from "../../../features/applications/terminal/commands.js";
|
||||
import { clamp } from "../../../features/math/clamp.js";
|
||||
import { OutputLine } from "./OutputLine.jsx";
|
||||
import { InputLine } from "./InputLine.jsx";
|
||||
|
||||
const USERNAME = "user";
|
||||
const HOSTNAME = "prozilla-os";
|
||||
import { HOSTNAME, USERNAME } from "../../../constants/applications/terminal.js";
|
||||
import CommandsManager from "../../../features/applications/terminal/commands.js";
|
||||
|
||||
export function Terminal({ setTitle }) {
|
||||
const [inputKey, setInputKey] = useState(0);
|
||||
|
|
@ -37,28 +35,29 @@ export function Terminal({ setTitle }) {
|
|||
});
|
||||
};
|
||||
|
||||
const submitInput = (value) => {
|
||||
pushHistory({
|
||||
text: prefix + value,
|
||||
isInput: true,
|
||||
value
|
||||
});
|
||||
|
||||
setInputValue("");
|
||||
|
||||
const handleInput = (value) => {
|
||||
const rawInputValueStart = value.indexOf(" ") + 1;
|
||||
const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart);
|
||||
value = value.trim();
|
||||
|
||||
if (value === "")
|
||||
return;
|
||||
|
||||
const args = value.split(/ +/);
|
||||
|
||||
if (args[0].toLowerCase() === "sudo" && args.length > 1) {
|
||||
args.shift();
|
||||
}
|
||||
|
||||
const commandName = args.shift().toLowerCase();
|
||||
|
||||
const command = Command.find(commandName);
|
||||
const command = CommandsManager.find(commandName);
|
||||
|
||||
if (!command) {
|
||||
return promptOutput(`${commandName}: Command not found`);
|
||||
}
|
||||
if (!command)
|
||||
return `${commandName}: Command not found`;
|
||||
|
||||
if (command.requireArgs && args.length === 0)
|
||||
return `${commandName}: Incorrect syntax: ${commandName} requires at least 1 argument`;
|
||||
|
||||
let response = null;
|
||||
|
||||
|
|
@ -71,19 +70,43 @@ export function Terminal({ setTitle }) {
|
|||
setCurrentDirectory,
|
||||
username: USERNAME,
|
||||
hostname: HOSTNAME,
|
||||
rawInputValue
|
||||
});
|
||||
|
||||
if (response == null)
|
||||
return promptOutput(`${commandName}: Command failed`);
|
||||
return `${commandName}: Command failed`;
|
||||
|
||||
if (!response.blank)
|
||||
promptOutput(response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
promptOutput(`${commandName}: Command failed`);
|
||||
return `${commandName}: Command failed`;
|
||||
}
|
||||
};
|
||||
|
||||
const submitInput = (value) => {
|
||||
pushHistory({
|
||||
text: prefix + value,
|
||||
isInput: true,
|
||||
value
|
||||
});
|
||||
|
||||
setInputValue("");
|
||||
setHistoryIndex(0);
|
||||
|
||||
// Piping is used to chain commands
|
||||
const segments = value.split(" | ");
|
||||
|
||||
let output = null;
|
||||
segments.forEach((segment) => {
|
||||
// Output from the previous command gets added as an argument for the next command
|
||||
output = handleInput(output ? `${segment} ${output}` : segment);
|
||||
});
|
||||
|
||||
if (output)
|
||||
promptOutput(output);
|
||||
};
|
||||
|
||||
const updateHistoryIndex = (delta) => {
|
||||
const inputHistory = history.filter(({ isInput }) => isInput);
|
||||
const index = clamp(historyIndex + delta, 0, inputHistory.length);
|
||||
|
|
@ -142,7 +165,7 @@ export function Terminal({ setTitle }) {
|
|||
if (event.button === 2) {
|
||||
event.preventDefault();
|
||||
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
navigator.clipboard.readText?.().then((text) => {
|
||||
setInputValue(inputValue + text);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@
|
|||
letter-spacing: 0.01rem;
|
||||
}
|
||||
|
||||
.Terminal p {
|
||||
.Terminal p, .Terminal pre {
|
||||
margin: 0;
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
|
||||
.Prefix {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import styles from "./TextEditor.module.css";
|
|||
import { HeaderMenu } from "../.common/HeaderMenu.jsx";
|
||||
import Markdown from "markdown-to-jsx";
|
||||
import Application from "../../../features/applications/application.js";
|
||||
|
||||
const DEFAULT_ZOOM = 16;
|
||||
const ZOOM_FACTOR = 4;
|
||||
import { DEFAULT_ZOOM, ZOOM_FACTOR } from "../../../constants/applications/textEditor.js";
|
||||
|
||||
/**
|
||||
* @param {object} props
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ import { ReactSVG } from "react-svg";
|
|||
import utilStyles from "../../../styles/utils.module.css";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export const DIALOG_CONTENT_TYPES = {
|
||||
CloseButton: 0
|
||||
};
|
||||
import { DIALOG_CONTENT_TYPES } from "../../../constants/modals.js";
|
||||
|
||||
export function DialogBox({ modal, params }) {
|
||||
const { app, title, children } = params;
|
||||
|
|
|
|||
1
src/constants/applications/settings.js
Normal file
1
src/constants/applications/settings.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const WALLPAPERS_PATH = "~/Images/Wallpapers";
|
||||
3
src/constants/applications/terminal.js
Normal file
3
src/constants/applications/terminal.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const USERNAME = "user";
|
||||
export const HOSTNAME = "prozilla-os";
|
||||
export const MAX_WIDTH = 50;
|
||||
2
src/constants/applications/textEditor.js
Normal file
2
src/constants/applications/textEditor.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const DEFAULT_ZOOM = 16;
|
||||
export const ZOOM_FACTOR = 4;
|
||||
7
src/constants/modals.js
Normal file
7
src/constants/modals.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Vector2 from "../features/math/vector2.js";
|
||||
|
||||
export const DIALOG_CONTENT_TYPES = {
|
||||
CloseButton: 0
|
||||
};
|
||||
|
||||
export const DEFAULT_DIALOG_SIZE = new Vector2(400, 200);
|
||||
2
src/constants/windows.js
Normal file
2
src/constants/windows.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const SCREEN_MARGIN = 32;
|
||||
export const TASKBAR_HEIGHT = 48;
|
||||
67
src/features/applications/terminal/command.js
Normal file
67
src/features/applications/terminal/command.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { VirtualFolder } from "../../virtual-drive/virtualFolder.js";
|
||||
import { VirtualRoot } from "../../virtual-drive/virtualRoot.js";
|
||||
|
||||
export default class Command {
|
||||
/** @type {string} */
|
||||
name;
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @param {object} options
|
||||
* @param {Function} options.promptOutput
|
||||
* @param {Function} options.pushHistory
|
||||
* @param {VirtualRoot} options.virtualRoot
|
||||
* @param {VirtualFolder} options.currentDirectory
|
||||
* @param {Function} options.setCurrentDirectory
|
||||
* @param {string} options.username
|
||||
* @param {string} options.hostname
|
||||
* @param {string} options.rawInputValue
|
||||
* @returns {string|{ blank: boolean }}
|
||||
*/
|
||||
execute = (args, options) => {};
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Function} execute
|
||||
*/
|
||||
constructor(name, execute) {
|
||||
this.name = name;
|
||||
this.execute = execute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {Command}
|
||||
*/
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} execute
|
||||
* @returns {Command}
|
||||
*/
|
||||
setExecute(execute) {
|
||||
this.execute = execute;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @returns {Command}
|
||||
*/
|
||||
setRequireArgs(value) {
|
||||
this.requireArgs = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ purpose: string, usage: string, description: string}} name
|
||||
* @returns {Command}
|
||||
*/
|
||||
setManual({ purpose, usage, description }) {
|
||||
this.manual = { purpose, usage, description };
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,30 @@
|
|||
import { ASCII_LOGO } from "../../../constants/branding.js";
|
||||
import { START_DATE } from "../../../index.js";
|
||||
import { formatRelativeTime } from "../../utils/date.js";
|
||||
import ApplicationsManager from "../applications.js";
|
||||
import { blow } from "./commands/blow.js";
|
||||
import { cat } from "./commands/cat.js";
|
||||
import { cd } from "./commands/cd.js";
|
||||
import { clear } from "./commands/clear.js";
|
||||
import { cowsay } from "./commands/cowsay.js";
|
||||
import { dir } from "./commands/dir.js";
|
||||
import { echo } from "./commands/echo.js";
|
||||
import { fortune } from "./commands/fortune.js";
|
||||
import { hostname } from "./commands/hostname.js";
|
||||
import { ls } from "./commands/ls.js";
|
||||
import { make } from "./commands/make.js";
|
||||
import { man } from "./commands/man.js";
|
||||
import { mkdir } from "./commands/mkdir.js";
|
||||
import { neofetch } from "./commands/neofetch.js";
|
||||
import { nice } from "./commands/nice.js";
|
||||
import { pwd } from "./commands/pwd.js";
|
||||
import { reboot } from "./commands/reboot.js";
|
||||
import { rm } from "./commands/rm.js";
|
||||
import { rmdir } from "./commands/rmdir.js";
|
||||
import { touch } from "./commands/touch.js";
|
||||
import { world } from "./commands/world.js";
|
||||
|
||||
export class Command {
|
||||
export default class CommandsManager {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Function} execute
|
||||
* @returns {CommandsManager}
|
||||
*/
|
||||
constructor(name, execute) {
|
||||
this.name = name;
|
||||
this.execute = execute;
|
||||
this.aliases = [];
|
||||
}
|
||||
|
||||
addAlias(alias) {
|
||||
this.aliases.push(alias);
|
||||
}
|
||||
|
||||
static find(name) {
|
||||
let matchCommand = null;
|
||||
|
||||
|
|
@ -32,133 +39,26 @@ export class Command {
|
|||
}
|
||||
|
||||
static COMMANDS = [
|
||||
new Command("echo", (args) => {
|
||||
return args.join(" ");
|
||||
}),
|
||||
new Command("clear", (args, { pushHistory }) => {
|
||||
pushHistory({
|
||||
clear: true,
|
||||
isInput: false
|
||||
});
|
||||
|
||||
return { blank: true };
|
||||
}),
|
||||
new Command("ls", (args, { currentDirectory }) => {
|
||||
let directory = currentDirectory;
|
||||
|
||||
if (args.length > 0) {
|
||||
directory = currentDirectory.navigate(args[0]);
|
||||
}
|
||||
|
||||
if (!directory)
|
||||
return `ls: Cannot access '${args[0]}': No such file or directory`;
|
||||
|
||||
const folderNames = directory.subFolders.map((folder) => folder.id);
|
||||
const fileNames = directory.files.map((file) => file.id);
|
||||
|
||||
const contents = folderNames.concat(fileNames);
|
||||
|
||||
if (contents.length === 0)
|
||||
return { blank: true };
|
||||
|
||||
return contents.sort((nameA, nameB) => nameA.localeCompare(nameB)).join(" ");
|
||||
}),
|
||||
new Command("cd", (args, { currentDirectory, setCurrentDirectory }) => {
|
||||
const path = args[0] ?? "~";
|
||||
const destination = currentDirectory.navigate(path);
|
||||
|
||||
if (!destination)
|
||||
return `cd: ${args[0]}: No such file or directory`;
|
||||
|
||||
console.log(destination);
|
||||
setCurrentDirectory(destination);
|
||||
return { blank: true };
|
||||
}),
|
||||
new Command("dir", (args, { currentDirectory }) => {
|
||||
const folderNames = currentDirectory.subFolders.map((folder) => folder.id);
|
||||
|
||||
if (folderNames.length === 0)
|
||||
return { blank: true };
|
||||
|
||||
return folderNames.sort((nameA, nameB) => nameA.localeCompare(nameB)).join(" ");
|
||||
}),
|
||||
new Command("pwd", (args, { currentDirectory }) => {
|
||||
if (currentDirectory.root) {
|
||||
return "/";
|
||||
} else {
|
||||
return currentDirectory.absolutePath;
|
||||
}
|
||||
}),
|
||||
new Command("touch", (args, { currentDirectory }) => {
|
||||
const [name, extension] = args[0].split(".");
|
||||
|
||||
if (currentDirectory.findFile(name, extension))
|
||||
return { blank: true };
|
||||
|
||||
currentDirectory.createFile(name, extension);
|
||||
return { blank: true };
|
||||
}),
|
||||
new Command("mkdir", (args, { currentDirectory }) => {
|
||||
const name = args[0];
|
||||
|
||||
if (currentDirectory.findSubFolder(name))
|
||||
return { blank: true };
|
||||
|
||||
currentDirectory.createFolder(name);
|
||||
return { blank: true };
|
||||
}),
|
||||
new Command("rm", (args, { currentDirectory }) => {
|
||||
const [name, extension] = args[0].split(".");
|
||||
const file = currentDirectory.findFile(name, extension);
|
||||
|
||||
if (!file)
|
||||
return `rm: ${args[0]}: No such file`;
|
||||
|
||||
file.delete();
|
||||
return { blank: true };
|
||||
}),
|
||||
new Command("rmdir", (args, { currentDirectory }) => {
|
||||
const name = args[0];
|
||||
const folder = currentDirectory.findSubFolder(name);
|
||||
|
||||
if (!folder)
|
||||
return `rm: ${args[0]}: No such directory`;
|
||||
|
||||
folder.delete();
|
||||
return { blank: true };
|
||||
}),
|
||||
new Command("hostname", (args, { hostname }) => {
|
||||
return hostname;
|
||||
}),
|
||||
new Command("neofetch", (args, { username, hostname }) => {
|
||||
const leftColumn = ASCII_LOGO.split("\n");
|
||||
const rightColumnWidth = username.length + hostname.length + 1;
|
||||
const rightColumn = [
|
||||
`${username}@${hostname}`,
|
||||
"-".repeat(rightColumnWidth),
|
||||
"OS: ProzillaOS",
|
||||
`UPTIME: ${formatRelativeTime(START_DATE, 2, false)}`,
|
||||
`RESOLUTION: ${window.innerWidth}x${window.innerHeight}`,
|
||||
"THEME: default",
|
||||
"ICONS: Font Awesome",
|
||||
`TERMINAL: ${ApplicationsManager.getApplication("terminal")?.name ?? "Unknown"}`,
|
||||
];
|
||||
|
||||
const combined = [];
|
||||
for (let i = 1; i < leftColumn.length; i++) {
|
||||
let line = `${leftColumn[i]} `;
|
||||
|
||||
if (i <= rightColumn.length) {
|
||||
line += rightColumn[i - 1];
|
||||
} else {
|
||||
// This fixes a weird display bug on Safari mobile
|
||||
line += " ".repeat(rightColumnWidth);
|
||||
}
|
||||
|
||||
combined.push(line);
|
||||
}
|
||||
|
||||
return combined.join("\n");
|
||||
}),
|
||||
echo,
|
||||
clear,
|
||||
ls,
|
||||
cd,
|
||||
dir,
|
||||
pwd,
|
||||
touch,
|
||||
mkdir,
|
||||
rm,
|
||||
rmdir,
|
||||
hostname,
|
||||
neofetch,
|
||||
fortune,
|
||||
cowsay,
|
||||
world,
|
||||
nice,
|
||||
blow,
|
||||
make,
|
||||
cat,
|
||||
man,
|
||||
reboot,
|
||||
];
|
||||
}
|
||||
5
src/features/applications/terminal/commands/blow.js
Normal file
5
src/features/applications/terminal/commands/blow.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const blow = new Command("%blow", () => {
|
||||
return "fg: %blow: No such job";
|
||||
});
|
||||
24
src/features/applications/terminal/commands/cat.js
Normal file
24
src/features/applications/terminal/commands/cat.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const cat = new Command("cat")
|
||||
.setRequireArgs(true)
|
||||
.setManual({
|
||||
purpose: "Concetenate files and print on the standard output",
|
||||
usage: "cat [OPTION]... [FILE]...",
|
||||
description: "Concetenate FILE(s) to standard output."
|
||||
})
|
||||
.setExecute((args, { currentDirectory }) => {
|
||||
const [name, extension] = args[0].split(".");
|
||||
const file = currentDirectory.findFile(name, extension);
|
||||
|
||||
if (!file)
|
||||
return `rm: ${args[0]}: No such file`;
|
||||
|
||||
if (file.content) {
|
||||
return file.content;
|
||||
} else if (file.source) {
|
||||
return `Src: ${file.source}`;
|
||||
} else {
|
||||
return { blank: true };
|
||||
}
|
||||
});
|
||||
13
src/features/applications/terminal/commands/cd.js
Normal file
13
src/features/applications/terminal/commands/cd.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const cd = new Command("cd", (args, { currentDirectory, setCurrentDirectory }) => {
|
||||
const path = args[0] ?? "~";
|
||||
const destination = currentDirectory.navigate(path);
|
||||
|
||||
if (!destination)
|
||||
return `cd: ${args[0]}: No such file or directory`;
|
||||
|
||||
console.log(destination);
|
||||
setCurrentDirectory(destination);
|
||||
return { blank: true };
|
||||
}).setRequireArgs(true);
|
||||
10
src/features/applications/terminal/commands/clear.js
Normal file
10
src/features/applications/terminal/commands/clear.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const clear = new Command("clear", (args, { pushHistory }) => {
|
||||
pushHistory({
|
||||
clear: true,
|
||||
isInput: false
|
||||
});
|
||||
|
||||
return { blank: true };
|
||||
});
|
||||
99
src/features/applications/terminal/commands/cowsay.js
Normal file
99
src/features/applications/terminal/commands/cowsay.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { MAX_WIDTH } from "../../../../constants/applications/terminal.js";
|
||||
import Command from "../command.js";
|
||||
|
||||
const COW = `
|
||||
\\ ^__^
|
||||
\\ (oo)\\_______
|
||||
(__)\\ )\\/\\
|
||||
||----w |
|
||||
|| ||`;
|
||||
|
||||
export const cowsay = new Command("cowsay", (args, { rawInputValue }) => {
|
||||
// Separate input value into lines
|
||||
const segments = rawInputValue.split(" ");
|
||||
const lines = [];
|
||||
let currentLine = "";
|
||||
let maxLineWidth = 0;
|
||||
|
||||
const addLine = (line) => {
|
||||
line = line.trimEnd();
|
||||
lines.push(line);
|
||||
if (line.length > maxLineWidth)
|
||||
maxLineWidth = line.length;
|
||||
};
|
||||
|
||||
const nextLine = (word) => {
|
||||
addLine(currentLine);
|
||||
|
||||
if (word) {
|
||||
currentLine = word + " ";
|
||||
} else {
|
||||
currentLine = "";
|
||||
}
|
||||
};
|
||||
|
||||
segments.forEach((segment) => {
|
||||
// Add empty spaces preceding lines
|
||||
if (segment === "") {
|
||||
currentLine += " ";
|
||||
return;
|
||||
}
|
||||
|
||||
const words = segment.split("\n");
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const word = words[i];
|
||||
|
||||
// Handle next lines
|
||||
if (i > 0)
|
||||
nextLine();
|
||||
|
||||
// Fit word on current line
|
||||
if ((currentLine + word).length <= MAX_WIDTH) {
|
||||
currentLine += word + " ";
|
||||
} else if (word.length > MAX_WIDTH) {
|
||||
const remainingSpaces = MAX_WIDTH - currentLine.length;
|
||||
|
||||
if (remainingSpaces >= 2) {
|
||||
addLine(currentLine + word.substring(0, remainingSpaces - 1) + "-");
|
||||
currentLine = word.substring(remainingSpaces - 1) + " ";
|
||||
} else {
|
||||
nextLine(word);
|
||||
}
|
||||
} else {
|
||||
nextLine(word);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (currentLine.length > 0)
|
||||
addLine(currentLine);
|
||||
|
||||
// Turn lines into speech bubble
|
||||
const speechBubble = [` ${"_".repeat(maxLineWidth + 2)} `];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
const missingSpaces = maxLineWidth - line.length;
|
||||
|
||||
if (missingSpaces > 0)
|
||||
line += " ".repeat(missingSpaces);
|
||||
|
||||
if (lines.length > 1) {
|
||||
if (i === 0) {
|
||||
line = `/ ${line} \\`;
|
||||
} else if (i === lines.length - 1) {
|
||||
line = `\\ ${line} /`;
|
||||
} else {
|
||||
line = `| ${line} |`;
|
||||
}
|
||||
} else {
|
||||
line = `< ${line} >`;
|
||||
}
|
||||
|
||||
speechBubble.push(line);
|
||||
}
|
||||
|
||||
speechBubble.push(` ${"-".repeat(maxLineWidth + 2)} `);
|
||||
|
||||
return speechBubble.join("\n") + COW;
|
||||
}).setRequireArgs(true);
|
||||
10
src/features/applications/terminal/commands/dir.js
Normal file
10
src/features/applications/terminal/commands/dir.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const dir = new Command("dir", (args, { currentDirectory }) => {
|
||||
const folderNames = currentDirectory.subFolders.map((folder) => folder.id);
|
||||
|
||||
if (folderNames.length === 0)
|
||||
return { blank: true };
|
||||
|
||||
return folderNames.sort((nameA, nameB) => nameA.localeCompare(nameB)).join(" ");
|
||||
});
|
||||
5
src/features/applications/terminal/commands/echo.js
Normal file
5
src/features/applications/terminal/commands/echo.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const echo = new Command("echo", (args, { rawInputValue }) => {
|
||||
return rawInputValue;
|
||||
});
|
||||
99
src/features/applications/terminal/commands/fortune.js
Normal file
99
src/features/applications/terminal/commands/fortune.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { randomFromArray } from "../../../utils/array.js";
|
||||
import Command from "../command.js";
|
||||
|
||||
/**
|
||||
* Source: https://github.com/shlomif/fortune-mod/blob/master/fortune-mod/datfiles/fortunes
|
||||
*/
|
||||
const FORTUNES = [
|
||||
"A day for firm decisions!!!!! Or is it?",
|
||||
"Just to have it is enough.",
|
||||
"Keep emotionally active. Cater to your favorite neurosis.",
|
||||
"Keep it short for pithy sake.",
|
||||
"Lady Luck brings added income today. Lady friend takes it away tonight.",
|
||||
"Learn to pause -- or nothing worthwhile can catch up to you.",
|
||||
"Let me put it this way: today is going to be a learning experience.",
|
||||
"Life is to you a dashing and bold adventure.",
|
||||
"\"Life, loathe it or ignore it, you can't like it.\"\n\n -- Marvin, \"Hitchhiker's Guide to the Galaxy\"",
|
||||
"Live in a world of your own, but always welcome visitors.",
|
||||
"Living your life is a task so difficult, it has never been attempted before.",
|
||||
"Long life is in store for you.",
|
||||
"Look afar and see the end from the beginning.",
|
||||
"Love is in the offing. Be affectionate to one who adores you.",
|
||||
"Make a wish, it might come true.",
|
||||
"Many changes of mind and mood; do not hesitate too long.",
|
||||
"Never be led astray onto the path of virtue.",
|
||||
"Never commit yourself! Let someone else commit you.",
|
||||
"Never give an inch!",
|
||||
"Never look up when dragons fly overhead.",
|
||||
"Never reveal your best argument.",
|
||||
"Next Friday will not be your lucky day. As a matter of fact, you don't have a lucky day this year.",
|
||||
"Of course you have a purpose -- to find a purpose.",
|
||||
"People are beginning to notice you. Try dressing before you leave the house.",
|
||||
"Perfect day for scrubbing the floor and other exciting things.",
|
||||
"Questionable day.\n\nAsk somebody something.",
|
||||
"Reply hazy, ask again later.",
|
||||
"Save energy: be apathetic.",
|
||||
"Ships are safe in harbor, but they were never meant to stay there.",
|
||||
"Slow day. Practice crawling.",
|
||||
"Snow Day -- stay home.",
|
||||
"So this is it. We're going to die.",
|
||||
"So you're back... about time...",
|
||||
"Someone is speaking well of you.",
|
||||
"Someone is speaking well of you.\n\nHow unusual!",
|
||||
"Someone whom you reject today, will reject you tomorrow.",
|
||||
"Stay away from flying saucers today.",
|
||||
"Stay away from hurricanes for a while.",
|
||||
"Stay the curse.",
|
||||
"That secret you've been guarding, isn't.",
|
||||
"The time is right to make new friends.",
|
||||
"The whole world is a tuxedo and you are a pair of brown shoes.\n\n -- George Gobel",
|
||||
"There is a 20% chance of tomorrow.",
|
||||
"There is a fly on your nose.",
|
||||
"There was a phone call for you.",
|
||||
"You're not my type. For that matter, you're not even my species!!!",
|
||||
"You're ugly and your mother dresses you funny.",
|
||||
"You're working under a slight handicap. You happen to be human.",
|
||||
"You've been leading a dog's life. Stay off the furniture.",
|
||||
"Your aim is high and to the right.",
|
||||
"Your aims are high, and you are capable of much.",
|
||||
"Your analyst has you mixed up with another patient. Don't believe a thing he tells you.",
|
||||
"Your best consolation is the hope that the things you failed to get weren't really worth having.",
|
||||
"Your boss climbed the corporate ladder, wrong by wrong.",
|
||||
"Your boss is a few sandwiches short of a picnic.",
|
||||
"Your boyfriend takes chocolate from strangers.",
|
||||
"Your business will assume vast proportions.",
|
||||
"Your business will go through a period of considerable expansion.",
|
||||
"Your depth of comprehension may tend to make you lax in worldly ways.",
|
||||
"Your domestic life may be harmonious.",
|
||||
"Your fly might be open (but don't check it just now).",
|
||||
"Your goose is cooked. (Your current chick is burned up too!)",
|
||||
"Your heart is pure, and your mind clear, and your soul devout.",
|
||||
"Your ignorance cramps my conversation.",
|
||||
"Your life would be very empty if you had nothing to regret.",
|
||||
"Your love life will be happy and harmonious.",
|
||||
"Your love life will be... interesting.",
|
||||
"Your lover will never wish to leave you.",
|
||||
"Your lucky color has faded.",
|
||||
"Your lucky number has been disconnected.",
|
||||
"Your lucky number is 3552664958674928. Watch for it everywhere.",
|
||||
"Your mode of life will be changed for the better because of good news soon.",
|
||||
"Your mode of life will be changed for the better because of new developments.",
|
||||
"Your motives for doing whatever good deed you may have in mind will be misinterpreted by somebody.",
|
||||
"Your nature demands love and your happiness depends on it.",
|
||||
"Your object is to save the world, while still leading a pleasant life.",
|
||||
"Your own qualities will help prevent your advancement in the world.",
|
||||
"Your present plans will be successful.",
|
||||
"Your reasoning is excellent -- it's only your basic assumptions that are wrong.",
|
||||
"Your reasoning powers are good, and you are a fairly good planner.",
|
||||
"Your sister swims out to meet troop ships.",
|
||||
"Your society will be sought by people of taste and refinement.",
|
||||
"Your step will soil many countries.",
|
||||
"Your supervisor is thinking about you.",
|
||||
"Your talents will be recognized and suitably rewarded.",
|
||||
"Your temporary financial embarrassment will be relieved in a surprising manner.",
|
||||
"Your true value depends entirely on what you are compared with.",
|
||||
];
|
||||
|
||||
export const fortune = new Command("fortune", () => {
|
||||
return randomFromArray(FORTUNES);
|
||||
});
|
||||
5
src/features/applications/terminal/commands/hostname.js
Normal file
5
src/features/applications/terminal/commands/hostname.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const hostname = new Command("hostname", (args, { hostname }) => {
|
||||
return hostname;
|
||||
});
|
||||
29
src/features/applications/terminal/commands/ls.js
Normal file
29
src/features/applications/terminal/commands/ls.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const ls = new Command("ls")
|
||||
.setManual({
|
||||
purpose: "list directory contents",
|
||||
usage: "ls [OPTION]... [FILE]...",
|
||||
description: "List information about the FILEs (the current directory by default).\n"
|
||||
+ "Sort entries alphabetically if none of -cftuvSUX nor --sort is specified."
|
||||
})
|
||||
.setExecute((args, { currentDirectory }) => {
|
||||
let directory = currentDirectory;
|
||||
|
||||
if (args.length > 0) {
|
||||
directory = currentDirectory.navigate(args[0]);
|
||||
}
|
||||
|
||||
if (!directory)
|
||||
return `ls: Cannot access '${args[0]}': No such file or directory`;
|
||||
|
||||
const folderNames = directory.subFolders.map((folder) => folder.id);
|
||||
const fileNames = directory.files.map((file) => file.id);
|
||||
|
||||
const contents = folderNames.concat(fileNames);
|
||||
|
||||
if (contents.length === 0)
|
||||
return { blank: true };
|
||||
|
||||
return contents.sort((nameA, nameB) => nameA.localeCompare(nameB)).join(" ");
|
||||
});
|
||||
6
src/features/applications/terminal/commands/make.js
Normal file
6
src/features/applications/terminal/commands/make.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const make = new Command("make", (args) => {
|
||||
if (args[0] === "love")
|
||||
return "make: *** No rule to make target `love'. Stop.";
|
||||
});
|
||||
54
src/features/applications/terminal/commands/man.js
Normal file
54
src/features/applications/terminal/commands/man.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import Command from "../command.js";
|
||||
import CommandsManager from "../commands.js";
|
||||
|
||||
const MARGIN = 5;
|
||||
|
||||
export const man = new Command("man")
|
||||
.setRequireArgs(true)
|
||||
.setManual({
|
||||
purpose: "show system reference manuals",
|
||||
usage: "man [OPTION]... page",
|
||||
description: "Each page arguments given to man is normally the name of a command.\n"
|
||||
+ "The manual page associated with this command is then found and displayed."
|
||||
})
|
||||
.setExecute((args) => {
|
||||
const commandName = args[0].toLowerCase();
|
||||
const command = CommandsManager.find(commandName);
|
||||
|
||||
if (!command)
|
||||
return `man: ${commandName}: Command not found`;
|
||||
|
||||
const manual = command.manual;
|
||||
|
||||
if (!manual)
|
||||
return `man: ${commandName}: No manual found`;
|
||||
|
||||
const formatText = (text) => {
|
||||
const lines = text.split("\n").map((line) => " ".repeat(MARGIN) + line);
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
const sections = [["NAME"]];
|
||||
|
||||
if (manual.purpose) {
|
||||
sections[0].push(formatText(`${commandName} - ${command.manual.purpose}`));
|
||||
} else {
|
||||
sections[0].push(formatText(commandName));
|
||||
}
|
||||
|
||||
if (manual.usage) {
|
||||
sections.push([
|
||||
"SYNOPSIS",
|
||||
formatText(manual.usage)
|
||||
]);
|
||||
}
|
||||
|
||||
if (manual.description) {
|
||||
sections.push([
|
||||
"DESCRIPTION",
|
||||
formatText(manual.description)
|
||||
]);
|
||||
}
|
||||
|
||||
return sections.map((section) => section.join("\n")).join("\n\n");
|
||||
});
|
||||
11
src/features/applications/terminal/commands/mkdir.js
Normal file
11
src/features/applications/terminal/commands/mkdir.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const mkdir = new Command("mkdir", (args, { currentDirectory }) => {
|
||||
const name = args[0];
|
||||
|
||||
if (currentDirectory.findSubFolder(name))
|
||||
return { blank: true };
|
||||
|
||||
currentDirectory.createFolder(name);
|
||||
return { blank: true };
|
||||
}).setRequireArgs(true);
|
||||
56
src/features/applications/terminal/commands/neofetch.js
Normal file
56
src/features/applications/terminal/commands/neofetch.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { ASCII_LOGO, NAME } from "../../../../constants/branding.js";
|
||||
import { START_DATE } from "../../../../index.js";
|
||||
import { formatRelativeTime } from "../../../utils/date.js";
|
||||
import ApplicationsManager from "../../applications.js";
|
||||
import Command from "../command.js";
|
||||
|
||||
export const neofetch = new Command("neofetch", (args, { username, hostname }) => {
|
||||
const leftColumn = ASCII_LOGO.split("\n");
|
||||
const rightColumnWidth = username.length + hostname.length + 1;
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
// Check for the browser name using regular expressions
|
||||
let browserName;
|
||||
if (userAgent.match(/Firefox\//)) {
|
||||
browserName = "Mozilla Firefox";
|
||||
} else if (userAgent.match(/Edg\//)) {
|
||||
browserName = "Microsoft Edge";
|
||||
} else if (userAgent.match(/Chrome\//)) {
|
||||
browserName = "Google Chrome";
|
||||
} else if (userAgent.match(/Safari\//)) {
|
||||
browserName = "Apple Safari";
|
||||
} else {
|
||||
browserName = "Unknown";
|
||||
}
|
||||
|
||||
const rightColumn = [
|
||||
`${username}@${hostname}`,
|
||||
"-".repeat(rightColumnWidth),
|
||||
`OS: ${NAME}`,
|
||||
`UPTIME: ${formatRelativeTime(START_DATE, 2, false)}`,
|
||||
`RESOLUTION: ${window.innerWidth}x${window.innerHeight}`,
|
||||
"THEME: default",
|
||||
"ICONS: Font Awesome",
|
||||
`TERMINAL: ${ApplicationsManager.getApplication("terminal")?.name ?? "Unknown"}`,
|
||||
`BROWSER: ${browserName}`,
|
||||
`PLATFORM: ${navigator.platform}`,
|
||||
`LANGUAGE: ${navigator.language}`,
|
||||
];
|
||||
|
||||
const combined = [];
|
||||
for (let i = 1; i < leftColumn.length; i++) {
|
||||
let line = `${leftColumn[i]} `;
|
||||
|
||||
if (i <= rightColumn.length) {
|
||||
line += rightColumn[i - 1];
|
||||
} else {
|
||||
// This fixes a weird display bug on Safari mobile
|
||||
line += " ".repeat(rightColumnWidth);
|
||||
}
|
||||
|
||||
combined.push(line);
|
||||
}
|
||||
|
||||
return combined.join("\n");
|
||||
});
|
||||
6
src/features/applications/terminal/commands/nice.js
Normal file
6
src/features/applications/terminal/commands/nice.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const nice = new Command("nice", (args) => {
|
||||
if (args[0] === "man" && args[1] === "woman")
|
||||
return "nice: No manual entry for woman";
|
||||
});
|
||||
9
src/features/applications/terminal/commands/pwd.js
Normal file
9
src/features/applications/terminal/commands/pwd.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const pwd = new Command("pwd", (args, { currentDirectory }) => {
|
||||
if (currentDirectory.root) {
|
||||
return "/";
|
||||
} else {
|
||||
return currentDirectory.absolutePath;
|
||||
}
|
||||
});
|
||||
7
src/features/applications/terminal/commands/reboot.js
Normal file
7
src/features/applications/terminal/commands/reboot.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { reloadViewport } from "../../../utils/browser.js";
|
||||
import Command from "../command.js";
|
||||
|
||||
export const reboot = new Command("reboot", () => {
|
||||
reloadViewport();
|
||||
return { blank: true };
|
||||
});
|
||||
12
src/features/applications/terminal/commands/rm.js
Normal file
12
src/features/applications/terminal/commands/rm.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const rm = new Command("rm", (args, { currentDirectory }) => {
|
||||
const [name, extension] = args[0].split(".");
|
||||
const file = currentDirectory.findFile(name, extension);
|
||||
|
||||
if (!file)
|
||||
return `rm: ${args[0]}: No such file`;
|
||||
|
||||
file.delete();
|
||||
return { blank: true };
|
||||
}).setRequireArgs(true);
|
||||
12
src/features/applications/terminal/commands/rmdir.js
Normal file
12
src/features/applications/terminal/commands/rmdir.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const rmdir = new Command("rmdir", (args, { currentDirectory }) => {
|
||||
const name = args[0];
|
||||
const folder = currentDirectory.findSubFolder(name);
|
||||
|
||||
if (!folder)
|
||||
return `rmdir: ${args[0]}: No such directory`;
|
||||
|
||||
folder.delete();
|
||||
return { blank: true };
|
||||
}).setRequireArgs(true);
|
||||
22
src/features/applications/terminal/commands/touch.js
Normal file
22
src/features/applications/terminal/commands/touch.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const touch = new Command("touch")
|
||||
.setRequireArgs(true)
|
||||
.setManual({
|
||||
purpose: "change file timestamps",
|
||||
usage: "touch [OPTION]... FILE...",
|
||||
description: "Update the access and modification times of each FILE to the current time.\n\n"
|
||||
+ "A FILE argument that does not exist is created empty."
|
||||
})
|
||||
.setExecute((args, { currentDirectory }) => {
|
||||
if (args[0] === "girls\\" && args[1] === "boo**")
|
||||
return "touch: Cannot touch 'girls boo**': Permission denied";
|
||||
|
||||
const [name, extension] = args[0].split(".");
|
||||
|
||||
if (currentDirectory.findFile(name, extension))
|
||||
return { blank: true };
|
||||
|
||||
currentDirectory.createFile(name, extension);
|
||||
return { blank: true };
|
||||
});
|
||||
5
src/features/applications/terminal/commands/world.js
Normal file
5
src/features/applications/terminal/commands/world.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Command from "../command.js";
|
||||
|
||||
export const world = new Command("world", () => {
|
||||
return "world: Not found";
|
||||
});
|
||||
|
|
@ -7,4 +7,12 @@ export function removeFromArray(item, array) {
|
|||
if (index !== -1) {
|
||||
array.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*[]} array
|
||||
* @returns {*}
|
||||
*/
|
||||
export function randomFromArray(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import { SCREEN_MARGIN, TASKBAR_HEIGHT } from "../../constants/windows.js";
|
||||
import ApplicationsManager from "../applications/applications.js";
|
||||
import { randomRange } from "../math/random.js";
|
||||
import Vector2 from "../math/vector2.js";
|
||||
import { VirtualFile } from "../virtual-drive/virtualFile.js";
|
||||
|
||||
export const SCREEN_MARGIN = 32;
|
||||
export const TASKBAR_HEIGHT = 48;
|
||||
|
||||
export default class WindowsManager {
|
||||
constructor() {
|
||||
this.windows = {};
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@ import { useCallback } from "react";
|
|||
import Modal from "../../features/modals/modal.js";
|
||||
import { DialogBox } from "../../components/modals/dialog-box/DialogBox.jsx";
|
||||
import Vector2 from "../../features/math/vector2.js";
|
||||
|
||||
const DEFAULT_SIZE = new Vector2(400, 200);
|
||||
import { DEFAULT_DIALOG_SIZE } from "../../constants/modals.js";
|
||||
|
||||
export function useDialogBox({ modalsManager }) {
|
||||
const onDialogBox = useCallback((event, params = {}) => {
|
||||
const size = params.size ?? DEFAULT_SIZE;
|
||||
const size = params.size ?? DEFAULT_DIALOG_SIZE;
|
||||
let positionX = (window.innerWidth - size.x) / 4;
|
||||
let positionY = (window.innerHeight - size.y) / 4;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue