Added sl command + terminal streaming

This commit is contained in:
Prozilla 2024-04-28 23:16:07 +02:00
parent 5a592f854e
commit 96a77f6f2e
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
10 changed files with 254 additions and 22 deletions

18
.vscode/tasks.json vendored Normal file
View file

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

View file

@ -1,10 +1,10 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="6.10352e-05" width="200" height="200" rx="42" fill="#10AC84"/>
<g filter="url(#filter0_d_63_38)">
<path d="M162.07 57.04L142.63 142H109.51L99.6705 93.76L89.8305 142H56.7105L37.2705 57.04H65.9505L73.6305 110.56L85.1505 57.04H114.19L125.71 110.56L133.39 57.04H162.07Z" fill="#C8D6E5"/>
<path d="M66.1394 145L39.2594 59.56H63.1394L82.0994 125.32H71.0594L89.7794 59.56H109.579L128.299 125.32H117.379L136.339 59.56H159.979L133.099 145H113.179L94.4594 79.36H104.779L86.0594 145H66.1394Z" fill="#C8D6E5"/>
</g>
<defs>
<filter id="filter0_d_63_38" x="13.7411" y="33.5106" width="171.859" height="132.019" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter0_d_63_38" x="15.7299" y="36.0306" width="167.779" height="132.499" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 287 KiB

View file

@ -23,11 +23,14 @@
<meta property="og:image" content="%PUBLIC_URL%/assets/banner-logo-title.png"/>
<meta property="og:locale" content="en_US">
<!-- Twitter -->
<meta property="twitter:title" content="Prozilla OS | Web-based Operating System"/>
<meta property="twitter:description" content="Prozilla OS is an open-source web-based operating system inspired by Ubuntu Linux and Windows made with React.js by Prozilla."/>
<meta property="twitter:card" content="summary_large_image"/>
<meta property="twitter:image" content="%PUBLIC_URL%/assets/banner-logo-title.png"/>
<!-- Fonts -->
<link rel="preload" href="%PUBLIC_URL%/assets/fonts/poppins/Poppins-Light.ttf" as="font" type="font/ttf" crossorigin>
<link rel="preload" href="%PUBLIC_URL%/assets/fonts/poppins/Poppins-Regular.ttf" as="font" type="font/ttf" crossorigin>
<link rel="preload" href="%PUBLIC_URL%/assets/fonts/poppins/Poppins-SemiBold.ttf" as="font" type="font/ttf" crossorigin>
<link rel="preload" href="%PUBLIC_URL%/assets/fonts/poppins/Poppins-Bold.ttf" as="font" type="font/ttf" crossorigin>
<link rel="preload" href="%PUBLIC_URL%/assets/fonts/outfit/Outfit-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin>
<link rel="preload" href="%PUBLIC_URL%/assets/fonts/roboto-mono/RobotoMono-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin>
<!-- FAQ -->

View file

@ -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 (<div ref={ref}>
{lines.map((line, index) =>
<Ansi key={index} className={styles.Output} useClasses>{line === "" ? " " : line}</Ansi>
)}
</>);
}
</div>);
});

View file

@ -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 (
<div
ref={streamRef}
className={styles.Terminal}
onMouseDown={onMouseDown}
onContextMenu={onContextMenu}
@ -221,15 +271,18 @@ export function Terminal({ startPath, setTitle, close: exit }) {
}}
>
{displayHistory()}
<InputLine
key={inputKey}
value={inputValue}
prefix={prefix}
onKeyDown={onKeyDown}
onChange={onChange}
inputRef={inputRef}
history={history}
/>
{!stream
? <InputLine
key={inputKey}
value={inputValue}
prefix={prefix}
onKeyDown={onKeyDown}
onChange={onChange}
inputRef={inputRef}
history={history}
/>
: <OutputLine text={streamOutput ?? ""}/>
}
</div>
);
}

View file

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

View file

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

View file

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