diff --git a/src/components/apps/terminal/Terminal.jsx b/src/components/apps/terminal/Terminal.jsx index 862ff94..2bfb129 100644 --- a/src/components/apps/terminal/Terminal.jsx +++ b/src/components/apps/terminal/Terminal.jsx @@ -8,6 +8,7 @@ import { ANSI, HOSTNAME, USERNAME } from "../../../config/apps/terminal.config.j import CommandsManager from "../../../features/apps/terminal/commands.js"; import { removeFromArray } from "../../../features/_utils/array.utils.js"; import Stream from "../../../features/apps/terminal/stream.js"; +import { formatError } from "../../../features/apps/terminal/_utils/terminal.utils.js"; /** * @param {import("../../windows/WindowView.jsx").windowProps} props @@ -60,7 +61,7 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }) { }); }; - const connectStream = (stream) => { + const connectStream = (stream, pipes) => { setStream(stream); setStreamFocused(false); @@ -73,8 +74,23 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }) { let lastOutput = null; stream.on(Stream.EVENT_NAMES.NEW, (text) => { - lastOutput = text; - setStreamOutput(text); + let output = text; + pipes.forEach((pipe) => { + if (output instanceof Stream) + return; + + // Output from the previous command gets added as an argument for the next command + output = handleInput(output ? `${pipe} ${output}` : pipe); + }); + + if (output instanceof Stream) { + stream.stop(); + promptOutput(ANSI.fg.red + "Stream failed"); + return; + } + + lastOutput = output; + setStreamOutput(output); }); stream.on(Stream.EVENT_NAMES.STOP, () => { @@ -92,6 +108,7 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }) { const handleInput = (value) => { const rawInputValueStart = value.indexOf(" ") + 1; const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart); + const timestamp = Date.now(); value = value.trim(); if (value === "") @@ -109,7 +126,7 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }) { const command = CommandsManager.find(commandName); if (!command) - return CommandsManager.formatError(commandName, "Command not found"); + return formatError(commandName, "Command not found"); // Get options const options = []; @@ -145,10 +162,10 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }) { // Check usage if (command.requireArgs && args.length === 0) - return CommandsManager.formatError(commandName, `Incorrect usage: ${commandName} requires at least 1 argument`); + return formatError(commandName, `Incorrect usage: ${commandName} requires at least 1 argument`); if (command.requireOptions && options.length === 0) - return CommandsManager.formatError(commandName, `Incorrect usage: ${commandName} requires at least 1 option`); + return formatError(commandName, `Incorrect usage: ${commandName} requires at least 1 option`); // Execute command let response = null; @@ -165,17 +182,18 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }) { rawInputValue, options, exit, - inputs + inputs, + timestamp }); if (response == null) - return CommandsManager.formatError(commandName, "Command failed"); + return formatError(commandName, "Command failed"); if (!response.blank) return response; } catch (error) { console.error(error); - return CommandsManager.formatError(commandName, "Command failed"); + return formatError(commandName, "Command failed"); } }; @@ -190,17 +208,21 @@ export function Terminal({ startPath, input, setTitle, close: exit, active }) { setHistoryIndex(0); // Piping is used to chain commands - const segments = value.split(" | "); + const pipes = value.split(" | "); let output = null; - segments.forEach((segment) => { + pipes.forEach((pipe) => { + if (output instanceof Stream) + return; + // Output from the previous command gets added as an argument for the next command - output = handleInput(output ? `${segment} ${output}` : segment); + output = handleInput(output ? `${pipe} ${output}` : pipe); + removeFromArray(pipe, pipes); }); if (output) { if (output instanceof Stream) { - connectStream(output); + connectStream(output, pipes); } else { promptOutput(`${output}\n`); } diff --git a/src/components/apps/terminal/Terminal.module.css b/src/components/apps/terminal/Terminal.module.css index d8eb5e8..b460c64 100644 --- a/src/components/apps/terminal/Terminal.module.css +++ b/src/components/apps/terminal/Terminal.module.css @@ -93,31 +93,31 @@ cursor: text; } -.ansi-black-fg { color: var(--dark-grey-e); } -.ansi-red-fg { color: var(--red-b); } -.ansi-green-fg { color: var(--green-b); } -.ansi-yellow-fg { color: var(--yellow-b); } -.ansi-blue-fg { color: var(--blue-b); } -.ansi-magenta-fg { color: var(--purple-b); } -.ansi-cyan-fg { color: var(--cyan-b); } -.ansi-white-fg { color: var(--grey-a); } +.ansi-black-fg { color: var(--dark-grey-c); } +.ansi-red-fg { color: var(--red-a); } +.ansi-green-fg { color: var(--green-a); } +.ansi-yellow-fg { color: var(--yellow-a); } +.ansi-blue-fg { color: var(--blue-a); } +.ansi-magenta-fg { color: var(--purple-a); } +.ansi-cyan-fg { color: var(--cyan-a); } +.ansi-white-fg { color: var(--foreground-color-a); } -.ansi-bright-black-fg { color: var(--dark-grey-d); } +.ansi-bright-black-fg { color: var(--dark-grey-c); } .ansi-bright-red-fg { color: var(--red-a); } .ansi-bright-green-fg { color: var(--green-a); } .ansi-bright-yellow-fg { color: var(--yellow-a); } .ansi-bright-blue-fg { color: var(--blue-a); } .ansi-bright-magenta-fg { color: var(--purple-a); } .ansi-bright-cyan-fg { color: var(--cyan-a); } -.ansi-bright-white-fg { color: var(--white-a); } +.ansi-bright-white-fg { color: var(--foreground-color-a); } -.ansi-black-bg { background-color: var(--dark-grey-d); } +.ansi-black-bg { background-color: var(--dark-grey-c); } .ansi-red-bg { background-color: var(--red-a); } .ansi-green-bg { background-color: var(--green-a); } .ansi-yellow-bg { background-color: var(--yellow-a); } .ansi-blue-bg { background-color: var(--blue-a); } .ansi-magenta-bg { background-color: var(--purple-a); } .ansi-cyan-bg { background-color: var(--cyan-a); } -.ansi-white-bg { background-color: var(--white-a); } +.ansi-white-bg { background-color: var(--foreground-color-a); } .ansi-dim { opacity: 0.65; } \ No newline at end of file diff --git a/src/features/apps/terminal/_utils/terminal.utils.js b/src/features/apps/terminal/_utils/terminal.utils.js new file mode 100644 index 0000000..24f2307 --- /dev/null +++ b/src/features/apps/terminal/_utils/terminal.utils.js @@ -0,0 +1,19 @@ +import { ANSI } from "../../../../config/apps/terminal.config.js"; + +/** + * @param {string} commandName + * @param {string} error + * @returns {string} + */ +export function formatError(commandName, error) { + return `${ANSI.fg.red}${commandName}: ${error}${ANSI.reset}`; +} + +/** + * @param {string} string + * @returns {string} + */ +export function removeAnsi(string) { + // eslint-disable-next-line no-control-regex + return string.replace(/\u001b\[([0-9]+)m/gm, ""); +} \ No newline at end of file diff --git a/src/features/apps/terminal/commands.js b/src/features/apps/terminal/commands.js index 004e62f..307a9d6 100644 --- a/src/features/apps/terminal/commands.js +++ b/src/features/apps/terminal/commands.js @@ -1,4 +1,3 @@ -import { ANSI } from "../../../config/apps/terminal.config.js"; import Command from "./command.js"; let commands = []; @@ -55,13 +54,4 @@ export default class CommandsManager { loadCommands(); CommandsManager.COMMANDS = commands; } - - /** - * @param {string} commandName - * @param {string} error - * @returns {string} - */ - static formatError(commandName, error) { - return `${ANSI.fg.red}${commandName}: ${error}${ANSI.reset}`; - } } \ No newline at end of file diff --git a/src/features/apps/terminal/commands/cat.js b/src/features/apps/terminal/commands/cat.js index b2510ad..d297199 100644 --- a/src/features/apps/terminal/commands/cat.js +++ b/src/features/apps/terminal/commands/cat.js @@ -1,6 +1,6 @@ import { VirtualFile } from "../../../virtual-drive/file/virtualFile.js"; +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; -import CommandsManager from "../commands.js"; export const cat = new Command() .setRequireArgs(true) @@ -14,7 +14,7 @@ export const cat = new Command() const file = currentDirectory.findFile(name, extension); if (!file) - return CommandsManager.formatError(this.name, `${args[0]}: No such file`); + return formatError(this.name, `${args[0]}: No such file`); if (file.content) { if (!options.includes("e")) { diff --git a/src/features/apps/terminal/commands/cd.js b/src/features/apps/terminal/commands/cd.js index 28b9385..7c975fd 100644 --- a/src/features/apps/terminal/commands/cd.js +++ b/src/features/apps/terminal/commands/cd.js @@ -1,5 +1,5 @@ +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; -import CommandsManager from "../commands.js"; export const cd = new Command() .setManual({ @@ -12,7 +12,7 @@ export const cd = new Command() const destination = currentDirectory.navigate(path); if (!destination) - return CommandsManager.formatError(this.name, `${args[0]}: No such file or directory`); + return formatError(this.name, `${args[0]}: No such file or directory`); setCurrentDirectory(destination); return { blank: true }; diff --git a/src/features/apps/terminal/commands/help.js b/src/features/apps/terminal/commands/help.js index 2004607..a367847 100644 --- a/src/features/apps/terminal/commands/help.js +++ b/src/features/apps/terminal/commands/help.js @@ -1,4 +1,5 @@ import { ANSI } from "../../../../config/apps/terminal.config.js"; +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; import CommandsManager from "../commands.js"; @@ -18,10 +19,10 @@ export const help = new Command() const command = CommandsManager.find(commandName); if (!command) - return CommandsManager.formatError(this.name, `${commandName}: Command not found`); + return formatError(this.name, `${commandName}: Command not found`); if (!command.manual?.purpose) - return CommandsManager.formatError(this.name, `${commandName}: No manual found`); + return formatError(this.name, `${commandName}: No manual found`); return command.manual.purpose; }); \ No newline at end of file diff --git a/src/features/apps/terminal/commands/lolcat.js b/src/features/apps/terminal/commands/lolcat.js new file mode 100644 index 0000000..4858f86 --- /dev/null +++ b/src/features/apps/terminal/commands/lolcat.js @@ -0,0 +1,52 @@ +import { ANSI } from "../../../../config/apps/terminal.config.js"; +import { removeAnsi } from "../_utils/terminal.utils.js"; +import Command from "../command.js"; + +const COLUMN_WIDTH = 5; +const ROW_OFFSET = 2; + +const RAINBOW = [ + ANSI.fg.red, + ANSI.fg.yellow, + ANSI.fg.green, + ANSI.fg.cyan, + ANSI.fg.blue, + ANSI.fg.magenta, +]; + +export const lolcat = new Command() + .setManual({ + purpose: "Display text with a rainbow effect" + }) + .setExecute(function(args, { rawInputValue, timestamp }) { + let rows = removeAnsi(rawInputValue).split("\n"); + const offset = timestamp / 100; + + rows = rows.map((row, index) => { + const columns = []; + + const rowIndex = index + offset; + const rowOffset = COLUMN_WIDTH - ((ROW_OFFSET * rowIndex) % COLUMN_WIDTH); + let rainbowIndex = Math.floor(rowIndex / (COLUMN_WIDTH / ROW_OFFSET)); + + const addColumn = (start, end) => { + const column = row.substring(start, end); + const color = RAINBOW[rainbowIndex++ % RAINBOW.length]; + columns.push(color + column); + }; + + if (rowOffset > 0) + addColumn(0, rowOffset); + + for (let i = rowOffset; i < row.length; i += COLUMN_WIDTH + 1) { + addColumn(i, i + COLUMN_WIDTH + 1); + } + + if (row.length === 0) + return ""; + + return columns.join(""); + }); + + return rows.join("\n"); + }); \ No newline at end of file diff --git a/src/features/apps/terminal/commands/ls.js b/src/features/apps/terminal/commands/ls.js index 2ee34e3..0ef92da 100644 --- a/src/features/apps/terminal/commands/ls.js +++ b/src/features/apps/terminal/commands/ls.js @@ -1,6 +1,6 @@ import { ANSI } from "../../../../config/apps/terminal.config.js"; +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; -import CommandsManager from "../commands.js"; export const ls = new Command() .setManual({ @@ -16,7 +16,7 @@ export const ls = new Command() } if (!directory) - return CommandsManager.formatError(this.name, `Cannot access '${args[0]}': No such file or directory`); + return formatError(this.name, `Cannot access '${args[0]}': No such file or directory`); const folderNames = directory.subFolders.map((folder) => `${ANSI.fg.blue}${folder.id}${ANSI.reset}`); const fileNames = directory.files.map((file) => file.id); diff --git a/src/features/apps/terminal/commands/make.js b/src/features/apps/terminal/commands/make.js index 1cb9187..6263dc2 100644 --- a/src/features/apps/terminal/commands/make.js +++ b/src/features/apps/terminal/commands/make.js @@ -1,9 +1,9 @@ +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; -import CommandsManager from "../commands.js"; export const make = new Command() .setRequireArgs(true) .setExecute(function(args) { if (args[0] === "love") - return CommandsManager.formatError(this.name, "*** No rule to make target 'love'. Stop."); + return formatError(this.name, "*** No rule to make target 'love'. Stop."); }); \ No newline at end of file diff --git a/src/features/apps/terminal/commands/man.js b/src/features/apps/terminal/commands/man.js index 3d14fb2..901b9b5 100644 --- a/src/features/apps/terminal/commands/man.js +++ b/src/features/apps/terminal/commands/man.js @@ -1,4 +1,5 @@ import { ANSI } from "../../../../config/apps/terminal.config.js"; +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; import CommandsManager from "../commands.js"; @@ -33,12 +34,12 @@ export const man = new Command() const command = CommandsManager.find(commandName); if (!command) - return `${this.name}: ${commandName}: Command not found`; + return formatError(this.name, `${commandName}: Command not found`); const manual = command.manual; if (!manual) - return `${this.name}: ${commandName}: No manual found`; + return formatError(this.name, `${commandName}: No manual found`); const formatText = (text) => { const lines = text.split("\n").map((line) => " ".repeat(MARGIN) + line); diff --git a/src/features/apps/terminal/commands/rm.js b/src/features/apps/terminal/commands/rm.js index d455e01..75b6d7e 100644 --- a/src/features/apps/terminal/commands/rm.js +++ b/src/features/apps/terminal/commands/rm.js @@ -1,6 +1,6 @@ import { VirtualFile } from "../../../virtual-drive/file/virtualFile.js"; +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; -import CommandsManager from "../commands.js"; export const rm = new Command() .setRequireArgs(true) @@ -12,7 +12,7 @@ export const rm = new Command() const file = currentDirectory.findFile(name, extension); if (!file) - return CommandsManager.formatError(this.name, `${args[0]}: No such file`); + return formatError(this.name, `${args[0]}: No such file`); file.delete(); return { blank: true }; diff --git a/src/features/apps/terminal/commands/rmdir.js b/src/features/apps/terminal/commands/rmdir.js index 04eebc4..9186045 100644 --- a/src/features/apps/terminal/commands/rmdir.js +++ b/src/features/apps/terminal/commands/rmdir.js @@ -1,5 +1,5 @@ +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; -import CommandsManager from "../commands.js"; export const rmdir = new Command() .setRequireArgs(true) @@ -11,7 +11,7 @@ export const rmdir = new Command() const folder = currentDirectory.findSubFolder(name); if (!folder) - return CommandsManager.formatError(this.name, `${args[0]}: No such directory`); + return formatError(this.name, `${args[0]}: No such directory`); folder.delete(); return { blank: true }; diff --git a/src/features/apps/terminal/commands/sl.js b/src/features/apps/terminal/commands/sl.js index 5a6e590..d8a5533 100644 --- a/src/features/apps/terminal/commands/sl.js +++ b/src/features/apps/terminal/commands/sl.js @@ -1,5 +1,5 @@ +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; -import CommandsManager from "../commands.js"; import Stream from "../stream.js"; const ANIMATION_SPEED = 1.25; @@ -193,7 +193,7 @@ export const sl = new Command() wagonCount = parseInt(inputs.w); if (!wagonCount || wagonCount < 0) { - return CommandsManager.formatError(this.name, "Please specify a valid amount of wagons"); + return formatError(this.name, "Please specify a valid amount of wagons"); } } diff --git a/src/features/apps/terminal/commands/touch.js b/src/features/apps/terminal/commands/touch.js index 021ddfe..ecd7966 100644 --- a/src/features/apps/terminal/commands/touch.js +++ b/src/features/apps/terminal/commands/touch.js @@ -1,6 +1,6 @@ import { VirtualFile } from "../../../virtual-drive/file/virtualFile.js"; +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; -import CommandsManager from "../commands.js"; export const touch = new Command() .setRequireArgs(true) @@ -12,7 +12,7 @@ export const touch = new Command() }) .setExecute(function(args, { currentDirectory }) { if (args[0] === "girls\\" && args[1] === "boo**") - return CommandsManager.formatError(this.name, "Cannot touch 'girls boo**': Permission denied"); + return formatError(this.name, "Cannot touch 'girls boo**': Permission denied"); const { name, extension } = VirtualFile.convertId(args[0]); diff --git a/src/features/apps/terminal/commands/whatis.js b/src/features/apps/terminal/commands/whatis.js index 5f726c4..435d3e8 100644 --- a/src/features/apps/terminal/commands/whatis.js +++ b/src/features/apps/terminal/commands/whatis.js @@ -1,4 +1,5 @@ import { ANSI } from "../../../../config/apps/terminal.config.js"; +import { formatError } from "../_utils/terminal.utils.js"; import Command from "../command.js"; import CommandsManager from "../commands.js"; @@ -12,10 +13,10 @@ export const whatis = new Command() const command = CommandsManager.find(commandName); if (!command) - return CommandsManager.formatError(this.name, `${commandName}: Command not found`); + return formatError(this.name, `${commandName}: Command not found`); if (!command.manual?.purpose) - return CommandsManager.formatError(this.name, `${commandName}: No information found`); + return formatError(this.name, `${commandName}: No information found`); return `${commandName} - ${ANSI.fg.green}${command.manual.purpose}`; }); \ No newline at end of file diff --git a/src/styles/global/variables.css b/src/styles/global/variables.css index 44969f4..8f3bd8a 100644 --- a/src/styles/global/variables.css +++ b/src/styles/global/variables.css @@ -1,5 +1,5 @@ :root { - --white-a: #fff; + --white-a: hsl(210, 54%, 95%); --pink-a: #ff9ff3; --pink-b: #f368e0; --yellow-a: #feca57; @@ -16,8 +16,8 @@ --blue-b: #2e86de; --purple-a: #5f27cd; --purple-b: #341f97; - --grey-a: #c8d6e5; - --grey-b: #8395a7; + --grey-a: hsl(210, 36%, 85%); + --grey-b: hsl(210, 18%, 60%); --dark-grey-a: hsl(211, 14%, 40%); --dark-grey-b: hsl(var(--background-color-a-hsl)); --dark-grey-c: hsl(var(--background-color-c-hsl));