Updated terminal app

This commit is contained in:
Prozilla 2023-12-10 20:33:14 +01:00
parent 23062bcd29
commit c4868a040f
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
38 changed files with 688 additions and 184 deletions

View file

@ -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

View file

@ -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"

View file

@ -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>
)}
</>);
}

View file

@ -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);

View file

@ -15,8 +15,9 @@
letter-spacing: 0.01rem;
}
.Terminal p {
.Terminal p, .Terminal pre {
margin: 0;
min-height: 1.25rem;
}
.Prefix {

View file

@ -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

View file

@ -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;

View file

@ -0,0 +1 @@
export const WALLPAPERS_PATH = "~/Images/Wallpapers";

View file

@ -0,0 +1,3 @@
export const USERNAME = "user";
export const HOSTNAME = "prozilla-os";
export const MAX_WIDTH = 50;

View file

@ -0,0 +1,2 @@
export const DEFAULT_ZOOM = 16;
export const ZOOM_FACTOR = 4;

7
src/constants/modals.js Normal file
View 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
View file

@ -0,0 +1,2 @@
export const SCREEN_MARGIN = 32;
export const TASKBAR_HEIGHT = 48;

View 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;
}
}

View file

@ -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,
];
}

View file

@ -0,0 +1,5 @@
import Command from "../command.js";
export const blow = new Command("%blow", () => {
return "fg: %blow: No such job";
});

View 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 };
}
});

View 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);

View 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 };
});

View 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);

View 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(" ");
});

View file

@ -0,0 +1,5 @@
import Command from "../command.js";
export const echo = new Command("echo", (args, { rawInputValue }) => {
return rawInputValue;
});

View 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);
});

View file

@ -0,0 +1,5 @@
import Command from "../command.js";
export const hostname = new Command("hostname", (args, { hostname }) => {
return hostname;
});

View 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(" ");
});

View 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.";
});

View 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");
});

View 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);

View 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");
});

View 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";
});

View 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;
}
});

View 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 };
});

View 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);

View 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);

View 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 };
});

View file

@ -0,0 +1,5 @@
import Command from "../command.js";
export const world = new Command("world", () => {
return "world: Not found";
});

View file

@ -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)];
}

View file

@ -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 = {};

View file

@ -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;