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">
|
<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"/>
|
<rect y="6.10352e-05" width="200" height="200" rx="42" fill="#10AC84"/>
|
||||||
<g filter="url(#filter0_d_63_38)">
|
<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>
|
</g>
|
||||||
<defs>
|
<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"/>
|
<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"/>
|
<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/>
|
<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:image" content="%PUBLIC_URL%/assets/banner-logo-title.png"/>
|
||||||
<meta property="og:locale" content="en_US">
|
<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 -->
|
<!-- 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/outfit/Outfit-VariableFont_wght.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/roboto-mono/RobotoMono-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 -->
|
<!-- FAQ -->
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
|
import { forwardRef } from "react";
|
||||||
import Ansi from "./Ansi.jsx";
|
import Ansi from "./Ansi.jsx";
|
||||||
import styles from "./Terminal.module.css";
|
import styles from "./Terminal.module.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} props
|
* @param {object} props
|
||||||
* @param {string} props.text
|
* @param {string} props.text
|
||||||
|
* @param {*} props.ref
|
||||||
*/
|
*/
|
||||||
export function OutputLine({ text }) {
|
export const OutputLine = forwardRef(({ text }, ref) => {
|
||||||
const lines = text?.split("\n");
|
const lines = text?.split("\n");
|
||||||
|
|
||||||
return (<>
|
return (<div ref={ref}>
|
||||||
{lines.map((line, index) =>
|
{lines.map((line, index) =>
|
||||||
<Ansi key={index} className={styles.Output} useClasses>{line === "" ? " " : line}</Ansi>
|
<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 { ANSI, HOSTNAME, USERNAME } from "../../../config/apps/terminal.config.js";
|
||||||
import CommandsManager from "../../../features/apps/terminal/commands.js";
|
import CommandsManager from "../../../features/apps/terminal/commands.js";
|
||||||
import { removeFromArray } from "../../../features/_utils/array.utils.js";
|
import { removeFromArray } from "../../../features/_utils/array.utils.js";
|
||||||
|
import Stream from "../../../features/apps/terminal/stream.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("../../windows/WindowView.jsx").windowProps} props
|
* @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 [inputKey, setInputKey] = useState(0);
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [history, setHistory] = useState([]);
|
const [history, setHistory] = useState([]);
|
||||||
|
|
@ -19,11 +20,23 @@ export function Terminal({ startPath, setTitle, close: exit }) {
|
||||||
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~"));
|
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate(startPath ?? "~"));
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const [historyIndex, setHistoryIndex] = useState(0);
|
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(() => {
|
useEffect(() => {
|
||||||
setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`);
|
setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`);
|
||||||
}, [currentDirectory.path, currentDirectory.root, setTitle]);
|
}, [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}:`
|
const prefix = `${ANSI.fg.cyan + USERNAME}@${HOSTNAME + ANSI.reset}:`
|
||||||
+ `${ANSI.fg.blue + (currentDirectory.root ? "/" : currentDirectory.path) + 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 handleInput = (value) => {
|
||||||
const rawInputValueStart = value.indexOf(" ") + 1;
|
const rawInputValueStart = value.indexOf(" ") + 1;
|
||||||
const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart);
|
const rawInputValue = rawInputValueStart <= 0 ? "" : value.substr(rawInputValueStart);
|
||||||
|
|
@ -134,8 +176,13 @@ export function Terminal({ startPath, setTitle, close: exit }) {
|
||||||
output = handleInput(output ? `${segment} ${output}` : segment);
|
output = handleInput(output ? `${segment} ${output}` : segment);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (output)
|
if (output) {
|
||||||
promptOutput(`${output}\n`);
|
if (output instanceof Stream) {
|
||||||
|
connectStream(output);
|
||||||
|
} else {
|
||||||
|
promptOutput(`${output}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateHistoryIndex = (delta) => {
|
const updateHistoryIndex = (delta) => {
|
||||||
|
|
@ -170,6 +217,8 @@ export function Terminal({ startPath, setTitle, close: exit }) {
|
||||||
updateHistoryIndex(1);
|
updateHistoryIndex(1);
|
||||||
} else if (key === "ArrowDown") {
|
} else if (key === "ArrowDown") {
|
||||||
updateHistoryIndex(-1);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={streamRef}
|
||||||
className={styles.Terminal}
|
className={styles.Terminal}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
|
@ -221,15 +271,18 @@ export function Terminal({ startPath, setTitle, close: exit }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{displayHistory()}
|
{displayHistory()}
|
||||||
<InputLine
|
{!stream
|
||||||
key={inputKey}
|
? <InputLine
|
||||||
value={inputValue}
|
key={inputKey}
|
||||||
prefix={prefix}
|
value={inputValue}
|
||||||
onKeyDown={onKeyDown}
|
prefix={prefix}
|
||||||
onChange={onChange}
|
onKeyDown={onKeyDown}
|
||||||
inputRef={inputRef}
|
onChange={onChange}
|
||||||
history={history}
|
inputRef={inputRef}
|
||||||
/>
|
history={history}
|
||||||
|
/>
|
||||||
|
: <OutputLine text={streamOutput ?? ""}/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import { pwd } from "./commands/pwd.js";
|
||||||
import { reboot } from "./commands/reboot.js";
|
import { reboot } from "./commands/reboot.js";
|
||||||
import { rm } from "./commands/rm.js";
|
import { rm } from "./commands/rm.js";
|
||||||
import { rmdir } from "./commands/rmdir.js";
|
import { rmdir } from "./commands/rmdir.js";
|
||||||
|
import { sl } from "./commands/sl.js";
|
||||||
import { touch } from "./commands/touch.js";
|
import { touch } from "./commands/touch.js";
|
||||||
import { uptime } from "./commands/uptime.js";
|
import { uptime } from "./commands/uptime.js";
|
||||||
import { whatis } from "./commands/whatis.js";
|
import { whatis } from "./commands/whatis.js";
|
||||||
|
|
@ -76,5 +77,6 @@ export default class CommandsManager {
|
||||||
exit,
|
exit,
|
||||||
help,
|
help,
|
||||||
uptime,
|
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