Added sl command + terminal streaming
This commit is contained in:
parent
5a592f854e
commit
96a77f6f2e
10 changed files with 254 additions and 22 deletions
18
.vscode/tasks.json
vendored
Normal file
18
.vscode/tasks.json
vendored
Normal 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",
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>);
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
107
src/features/apps/terminal/commands/sl.js
Normal file
107
src/features/apps/terminal/commands/sl.js
Normal 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;
|
||||
});
|
||||
47
src/features/apps/terminal/stream.js
Normal file
47
src/features/apps/terminal/stream.js
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue