diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..fc0078c --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start Prozilla OS", + "type": "shell", + "command": "npm start", + "group": "none", + "presentation": { + "reveal": "always", + "panel": "shared", + }, + "runOptions": { + "runOn": "folderOpen", + } + }, + ] +} \ No newline at end of file diff --git a/public/assets/apps/icons/wordle.svg b/public/assets/apps/icons/wordle.svg index 311914a..9a8724a 100644 --- a/public/assets/apps/icons/wordle.svg +++ b/public/assets/apps/icons/wordle.svg @@ -1,10 +1,10 @@ - + - + diff --git a/public/assets/banner-logo-title-small.png b/public/assets/banner-logo-title-small.png index 544913f..b7f7015 100644 Binary files a/public/assets/banner-logo-title-small.png and b/public/assets/banner-logo-title-small.png differ diff --git a/public/assets/banner-logo-title.png b/public/assets/banner-logo-title.png index 0e523d3..2d3951b 100644 Binary files a/public/assets/banner-logo-title.png and b/public/assets/banner-logo-title.png differ diff --git a/public/index.html b/public/index.html index c764127..5d3d426 100644 --- a/public/index.html +++ b/public/index.html @@ -23,11 +23,14 @@ + + + + + + - - - - + diff --git a/src/components/apps/terminal/OutputLine.jsx b/src/components/apps/terminal/OutputLine.jsx index 8cf7586..587d3da 100644 --- a/src/components/apps/terminal/OutputLine.jsx +++ b/src/components/apps/terminal/OutputLine.jsx @@ -1,16 +1,18 @@ +import { forwardRef } from "react"; import Ansi from "./Ansi.jsx"; import styles from "./Terminal.module.css"; /** * @param {object} props * @param {string} props.text + * @param {*} props.ref */ -export function OutputLine({ text }) { +export const OutputLine = forwardRef(({ text }, ref) => { const lines = text?.split("\n"); - return (<> + return (
{lines.map((line, index) => {line === "" ? " " : line} )} - ); -} \ No newline at end of file +
); +}); \ No newline at end of file diff --git a/src/components/apps/terminal/Terminal.jsx b/src/components/apps/terminal/Terminal.jsx index b378430..5676b5c 100644 --- a/src/components/apps/terminal/Terminal.jsx +++ b/src/components/apps/terminal/Terminal.jsx @@ -7,11 +7,12 @@ import { InputLine } from "./InputLine.jsx"; import { ANSI, HOSTNAME, USERNAME } from "../../../config/apps/terminal.config.js"; import CommandsManager from "../../../features/apps/terminal/commands.js"; import { removeFromArray } from "../../../features/_utils/array.utils.js"; +import Stream from "../../../features/apps/terminal/stream.js"; /** * @param {import("../../windows/WindowView.jsx").windowProps} props */ -export function Terminal({ startPath, setTitle, close: exit }) { +export function Terminal({ startPath, setTitle, close: exit, active }) { const [inputKey, setInputKey] = useState(0); const [inputValue, setInputValue] = useState(""); const [history, setHistory] = useState([]); @@ -19,11 +20,23 @@ export function Terminal({ startPath, setTitle, close: exit }) { const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~")); const inputRef = useRef(null); const [historyIndex, setHistoryIndex] = useState(0); + const [stream, setStream] = useState(null); + const [streamOutput, setStreamOutput] = useState(null); + const streamRef = useRef(null); + const [streamFocused, setStreamFocused] = useState(false); useEffect(() => { setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`); }, [currentDirectory.path, currentDirectory.root, setTitle]); + useEffect(() => { + if (streamFocused || streamRef.current == null || streamOutput == null) + return; + + streamRef.current.scrollTop = streamRef.current.scrollHeight; + setStreamFocused(true); + }, [streamFocused, streamOutput, streamRef]); + const prefix = `${ANSI.fg.cyan + USERNAME}@${HOSTNAME + ANSI.reset}:` + `${ANSI.fg.blue + (currentDirectory.root ? "/" : currentDirectory.path) + ANSI.reset}$ `; @@ -40,6 +53,35 @@ export function Terminal({ startPath, setTitle, close: exit }) { }); }; + const connectStream = (stream) => { + setStream(stream); + setStreamFocused(false); + + const onKeyDown = (event) => { + if (active && (event.ctrlKey || event.metaKey) && event.key === "c") { + stream.stop(); + } + }; + + let lastOutput = null; + + stream.on(Stream.EVENT_NAMES.NEW, (text) => { + lastOutput = text; + setStreamOutput(text); + }); + + stream.on(Stream.EVENT_NAMES.STOP, () => { + document.removeEventListener("keydown", onKeyDown); + + promptOutput(lastOutput); + + setStream(null); + setStreamOutput(null); + }); + + document.addEventListener("keydown", onKeyDown); + }; + const handleInput = (value) => { const rawInputValueStart = value.indexOf(" ") + 1; const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart); @@ -134,8 +176,13 @@ export function Terminal({ startPath, setTitle, close: exit }) { output = handleInput(output ? `${segment} ${output}` : segment); }); - if (output) - promptOutput(`${output}\n`); + if (output) { + if (output instanceof Stream) { + connectStream(output); + } else { + promptOutput(`${output}\n`); + } + } }; const updateHistoryIndex = (delta) => { @@ -170,6 +217,8 @@ export function Terminal({ startPath, setTitle, close: exit }) { updateHistoryIndex(1); } else if (key === "ArrowDown") { updateHistoryIndex(-1); + } else if (!stream && (event.ctrlKey || event.metaKey) && key === "c") { + setInputValue((value) => value + "^C"); } }; @@ -210,6 +259,7 @@ export function Terminal({ startPath, setTitle, close: exit }) { return (
{displayHistory()} - + {!stream + ? + : + }
); } \ No newline at end of file diff --git a/src/features/apps/terminal/commands.js b/src/features/apps/terminal/commands.js index c6fe52c..fea3595 100644 --- a/src/features/apps/terminal/commands.js +++ b/src/features/apps/terminal/commands.js @@ -19,6 +19,7 @@ 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 { sl } from "./commands/sl.js"; import { touch } from "./commands/touch.js"; import { uptime } from "./commands/uptime.js"; import { whatis } from "./commands/whatis.js"; @@ -76,5 +77,6 @@ export default class CommandsManager { exit, help, uptime, + sl, ]; } \ No newline at end of file diff --git a/src/features/apps/terminal/commands/sl.js b/src/features/apps/terminal/commands/sl.js new file mode 100644 index 0000000..faacb01 --- /dev/null +++ b/src/features/apps/terminal/commands/sl.js @@ -0,0 +1,107 @@ +import Command from "../command.js"; +import Stream from "../stream.js"; + +const LOCOMOTIVE_SMOKE = [ + [ + " (@@) ( ) (@) ( ) @@ () @ o @ o", + " ( )", + " (@@@@)", + " ( )", + "", + " (@@@)", + ], + [ + " ( ) (@@) ( ) (@) () @@ o @ o", + " (@@@)", + " ( )", + " (@@@@)", + "", + " ( )", + ] +]; + +const LOCOMOTIVE_TOP = [ + " ==== ________ ___________ ", + " _D _| |_______/ \\__I_I_____===__|_________| ", + " |(_)--- | H\\________/ | | =|___ ___| ", + " / | | H | | | | ||_| |_|| ", + " | | | H |__--------------------| [___] | ", + " | ________|___H__/__|_____/[][]~\\_______| | ", + " |/ | |-----------I_____I [][] [] D |=======|__ ", +]; + +const LOCOMOTIVE_BOTTOM = [ + [ + "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ", + " |/-=|___|= || || || |_____/~\\___/ ", + " \\_/ \\O=====O=====O=====O_/ \\_/ ", + ], + [ + "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ", + " |/-=|___|=O=====O=====O=====O |_____/~\\___/ ", + " \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ", + ], + [ + "__/ =| o |=-O=====O=====O=====O \\ ____Y___________|__ ", + " |/-=|___|= || || || |_____/~\\___/ ", + " \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ", + ], + [ + "__/ =| o |=-~O=====O=====O=====O\\ ____Y___________|__ ", + " |/-=|___|= || || || |_____/~\\___/ ", + " \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ", + ], + [ + "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ", + " |/-=|___|= O=====O=====O=====O|_____/~\\___/ ", + " \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ", + ], + [ + "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ", + " |/-=|___|= || || || |_____/~\\___/ ", + " \\_/ \\_O=====O=====O=====O/ \\_/ ", + ] +].reverse(); + +function getLocomotive(frame) { + const smoke = LOCOMOTIVE_SMOKE[Math.round(frame / 6) % LOCOMOTIVE_SMOKE.length]; + const top = LOCOMOTIVE_TOP; + const bottom = LOCOMOTIVE_BOTTOM[frame % LOCOMOTIVE_BOTTOM.length]; + + const distance = 50 - frame; + const locomotive = smoke.concat(top, bottom).map((line) => { + if (distance === 0) { + return line; + } else if (distance > 0) { + return " ".repeat(distance) + line; + } else { + line = line.slice(-distance); + return line; + } + }).join("\n"); + + return `\n${locomotive}\n`; +} + +export const sl = new Command("sl") + .setExecute(function() { + const stream = new Stream(); + + let frame = 0; + const interval = setInterval(() => { + const text = getLocomotive(frame); + stream.send(text); + frame++; + + if (text.trim().length === 0) + stream.stop(); + }, 100); + + stream.on(Stream.EVENT_NAMES.STOP, () => { + clearInterval(interval); + }); + + stream.start(); + + return stream; + }); \ No newline at end of file diff --git a/src/features/apps/terminal/stream.js b/src/features/apps/terminal/stream.js new file mode 100644 index 0000000..3902dc1 --- /dev/null +++ b/src/features/apps/terminal/stream.js @@ -0,0 +1,47 @@ +import { EventEmitter } from "../../_utils/event.utils.js"; + +export default class Stream extends EventEmitter { + static EVENT_NAMES = { + NEW: "new", + START: "start", + STOP: "stop", + }; + + enabled = false; + + /** + * @param {Function} callback + * @returns {Stream} + */ + start(callback) { + if (this.enabled) + return; + + callback?.(this); + this.enabled = true; + this.emit(Stream.EVENT_NAMES.START); + return this; + } + + /** + * @returns {Stream} + */ + stop() { + if (!this.enabled) + return; + + this.enabled = false; + this.emit(Stream.EVENT_NAMES.STOP); + return this; + } + + /** + * @param {string} text + * @returns {Stream} + */ + send(text) { + if (this.enabled) + this.emit(Stream.EVENT_NAMES.NEW, text); + return this; + } +} \ No newline at end of file