Added ansi support for terminal app
This commit is contained in:
parent
efced37fc6
commit
ceb115c0c7
14 changed files with 331 additions and 30 deletions
12
package-lock.json
generated
12
package-lock.json
generated
|
|
@ -15,7 +15,9 @@
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"anser": "^2.1.1",
|
||||||
"core-js": "^3.31.1",
|
"core-js": "^3.31.1",
|
||||||
|
"escape-carriage": "^1.3.1",
|
||||||
"markdown-to-jsx": "^7.2.1",
|
"markdown-to-jsx": "^7.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
@ -5108,6 +5110,11 @@
|
||||||
"ajv": "^6.9.1"
|
"ajv": "^6.9.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/anser": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/anser/-/anser-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ=="
|
||||||
|
},
|
||||||
"node_modules/ansi-escapes": {
|
"node_modules/ansi-escapes": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||||
|
|
@ -7328,6 +7335,11 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-carriage": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw=="
|
||||||
|
},
|
||||||
"node_modules/escape-html": {
|
"node_modules/escape-html": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
|
|
||||||
14
package.json
14
package.json
|
|
@ -21,7 +21,9 @@
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"anser": "^2.1.1",
|
||||||
"core-js": "^3.31.1",
|
"core-js": "^3.31.1",
|
||||||
|
"escape-carriage": "^1.3.1",
|
||||||
"markdown-to-jsx": "^7.2.1",
|
"markdown-to-jsx": "^7.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
@ -60,11 +62,19 @@
|
||||||
"error",
|
"error",
|
||||||
"double"
|
"double"
|
||||||
],
|
],
|
||||||
"object-curly-spacing": ["warn", "always"],
|
"object-curly-spacing": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
"default-case": "off",
|
"default-case": "off",
|
||||||
"arrow-parens": "error",
|
"arrow-parens": "error",
|
||||||
"space-infix-ops": "warn",
|
"space-infix-ops": "warn",
|
||||||
"react/no-multi-comp": ["error", { "ignoreStateless": true }],
|
"react/no-multi-comp": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignoreStateless": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"jsdoc/no-undefined-types": "warn",
|
"jsdoc/no-undefined-types": "warn",
|
||||||
"jsdoc/require-param": "warn",
|
"jsdoc/require-param": "warn",
|
||||||
"jsdoc/check-tag-names": "warn",
|
"jsdoc/check-tag-names": "warn",
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,8 @@
|
||||||
|
|
||||||
.Context-menu .Icon {
|
.Context-menu .Icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
width: 0.875rem;
|
width: 0.875rem;
|
||||||
height: 0.875rem;
|
height: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +92,25 @@
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Context-menu .Image-icon {
|
||||||
|
position: absolute;
|
||||||
|
width: 1rem;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Context-menu .Image-icon div {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Context-menu .Image-icon div > svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.Context-menu .Shortcut {
|
.Context-menu .Shortcut {
|
||||||
color: var(--foreground-color-b);
|
color: var(--foreground-color-b);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
184
src/components/applications/terminal/Ansi.jsx
Normal file
184
src/components/applications/terminal/Ansi.jsx
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import Anser, { AnserJsonEntry } from "anser";
|
||||||
|
import { escapeCarriageReturn } from "escape-carriage";
|
||||||
|
import * as React from "react";
|
||||||
|
import styles from "./Terminal.module.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This was copied from
|
||||||
|
* https://github.com/nteract/ansi-to-react/blob/master/src/index.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts ANSI strings into JSON output.
|
||||||
|
* @name ansiToJSON
|
||||||
|
* @function
|
||||||
|
* @param {string} input - The input string.
|
||||||
|
* @param {boolean=} use_classes - If `true`, HTML classes will be appended
|
||||||
|
* to the HTML output.
|
||||||
|
* @returns {AnserJsonEntry[]} The parsed input.
|
||||||
|
*/
|
||||||
|
function ansiToJSON(input, use_classes) {
|
||||||
|
input = escapeCarriageReturn(fixBackspace(input));
|
||||||
|
return Anser.ansiToJson(input, {
|
||||||
|
json: true,
|
||||||
|
remove_empty: true,
|
||||||
|
use_classes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a class string.
|
||||||
|
* @param {AnserJsonEntry} bundle
|
||||||
|
* @returns {string} class name(s)
|
||||||
|
*/
|
||||||
|
function createClass(bundle) {
|
||||||
|
const classNames = [];
|
||||||
|
|
||||||
|
if (bundle.bg) {
|
||||||
|
classNames.push(styles[`${bundle.bg}-bg`]);
|
||||||
|
}
|
||||||
|
if (bundle.fg) {
|
||||||
|
classNames.push(styles[`${bundle.fg}-fg`]);
|
||||||
|
}
|
||||||
|
if (bundle.decoration) {
|
||||||
|
classNames.push(styles[`ansi-${bundle.decoration}`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (classNames.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return classNames.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the style attribute.
|
||||||
|
* @param {AnserJsonEntry} bundle
|
||||||
|
* @returns {object} returns the style object
|
||||||
|
*/
|
||||||
|
function createStyle(bundle) {
|
||||||
|
const style = {};
|
||||||
|
if (bundle.bg) {
|
||||||
|
style.backgroundColor = `rgb(${bundle.bg})`;
|
||||||
|
}
|
||||||
|
if (bundle.fg) {
|
||||||
|
style.color = `rgb(${bundle.fg})`;
|
||||||
|
}
|
||||||
|
switch (bundle.decoration) {
|
||||||
|
case "bold":
|
||||||
|
style.fontWeight = "bold";
|
||||||
|
break;
|
||||||
|
case "dim":
|
||||||
|
style.opacity = "0.5";
|
||||||
|
break;
|
||||||
|
case "italic":
|
||||||
|
style.fontStyle = "italic";
|
||||||
|
break;
|
||||||
|
case "hidden":
|
||||||
|
style.visibility = "hidden";
|
||||||
|
break;
|
||||||
|
case "strikethrough":
|
||||||
|
style.textDecoration = "line-through";
|
||||||
|
break;
|
||||||
|
case "underline":
|
||||||
|
style.textDecoration = "underline";
|
||||||
|
break;
|
||||||
|
case "blink":
|
||||||
|
style.textDecoration = "blink";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an Anser bundle into a React Node.
|
||||||
|
* @param {boolean} linkify - whether links should be converting into clickable anchor tags.
|
||||||
|
* @param {boolean} useClasses - should render the span with a class instead of style.
|
||||||
|
* @param {AnserJsonEntry} bundle - Anser output.
|
||||||
|
* @param {number} key
|
||||||
|
*/
|
||||||
|
function convertBundleIntoReact(linkify, useClasses, bundle, key) {
|
||||||
|
const style = useClasses ? null : createStyle(bundle);
|
||||||
|
const className = useClasses ? createClass(bundle) : null;
|
||||||
|
|
||||||
|
if (!linkify) {
|
||||||
|
return React.createElement(
|
||||||
|
"pre",
|
||||||
|
{ style, key, className },
|
||||||
|
bundle.content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = [];
|
||||||
|
const linkRegex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let match;
|
||||||
|
while ((match = linkRegex.exec(bundle.content)) !== null) {
|
||||||
|
const [, pre, url] = match;
|
||||||
|
|
||||||
|
const startIndex = match.index + pre.length;
|
||||||
|
if (startIndex > index) {
|
||||||
|
content.push(bundle.content.substring(index, startIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the href we generate from the link is fully qualified. We assume http
|
||||||
|
// if it starts with a www because many sites don't support https
|
||||||
|
const href = url.startsWith("www.") ? `http://${url}` : url;
|
||||||
|
content.push(
|
||||||
|
React.createElement(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
key: index,
|
||||||
|
href,
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
`${url}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
index = linkRegex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < bundle.content.length) {
|
||||||
|
content.push(bundle.content.substring(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.createElement("span", { style, key, className }, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} props
|
||||||
|
* @param {string=} props.children
|
||||||
|
* @param {boolean=} props.linkify
|
||||||
|
* @param {string=} props.className
|
||||||
|
* @param {boolean=} props.useClasses
|
||||||
|
*/
|
||||||
|
export default function Ansi(props) {
|
||||||
|
const { className, useClasses, children, linkify } = props;
|
||||||
|
return React.createElement(
|
||||||
|
"code",
|
||||||
|
{ className },
|
||||||
|
ansiToJSON(children ?? "", useClasses ?? false).map(
|
||||||
|
convertBundleIntoReact.bind(null, linkify ?? false, useClasses ?? false)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is copied from the Jupyter Classic source code
|
||||||
|
// notebook/static/base/js/utils.js to handle \b in a way
|
||||||
|
// that is **compatible with Jupyter classic**. One can
|
||||||
|
// argue that this behavior is questionable:
|
||||||
|
// https://stackoverflow.com/questions/55440152/multiple-b-doesnt-work-as-expected-in-jupyter#
|
||||||
|
function fixBackspace(txt) {
|
||||||
|
let tmp = txt;
|
||||||
|
do {
|
||||||
|
txt = tmp;
|
||||||
|
// Cancel out anything-but-newline followed by backspace
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
tmp = txt.replace(/[^\n]\x08/gm, "");
|
||||||
|
} while (tmp.length < txt.length);
|
||||||
|
return txt;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import styles from "./Terminal.module.css";
|
import styles from "./Terminal.module.css";
|
||||||
|
import Ansi from "./Ansi.jsx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} props
|
* @param {object} props
|
||||||
|
|
@ -19,7 +20,7 @@ export function InputLine({ value, prefix, onChange, onKeyUp, onKeyDown, inputRe
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={styles.Input}>
|
<span className={styles.Input}>
|
||||||
{prefix && <p className={[styles.Prefix]}>{prefix}</p>}
|
{prefix && <Ansi className={[styles.Prefix]} useClasses>{prefix}</Ansi>}
|
||||||
<span className={styles["Input-container"]} style={{ "--cursor-offset": cursorPosition }}>
|
<span className={styles["Input-container"]} style={{ "--cursor-offset": cursorPosition }}>
|
||||||
<span aria-hidden="true">{value}</span>
|
<span aria-hidden="true">{value}</span>
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Ansi from "./Ansi.jsx";
|
||||||
import styles from "./Terminal.module.css";
|
import styles from "./Terminal.module.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -9,7 +10,7 @@ export function OutputLine({ text }) {
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
{lines.map((line, index) =>
|
{lines.map((line, index) =>
|
||||||
<pre key={index} className={styles.Output}>{line === "" ? " " : line}</pre>
|
<Ansi key={index} className={styles.Output} useClasses>{line === "" ? " " : line}</Ansi>
|
||||||
)}
|
)}
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
@ -4,19 +4,19 @@ import { useVirtualRoot } from "../../../hooks/virtual-drive/virtualRootContext.
|
||||||
import { clamp } from "../../../features/math/clamp.js";
|
import { clamp } from "../../../features/math/clamp.js";
|
||||||
import { OutputLine } from "./OutputLine.jsx";
|
import { OutputLine } from "./OutputLine.jsx";
|
||||||
import { InputLine } from "./InputLine.jsx";
|
import { InputLine } from "./InputLine.jsx";
|
||||||
import { HOSTNAME, USERNAME } from "../../../constants/applications/terminal.js";
|
import { ANSI, HOSTNAME, USERNAME } from "../../../constants/applications/terminal.js";
|
||||||
import CommandsManager from "../../../features/applications/terminal/commands.js";
|
import CommandsManager from "../../../features/applications/terminal/commands.js";
|
||||||
import { removeFromArray } from "../../../features/utils/array.js";
|
import { removeFromArray } from "../../../features/utils/array.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("../../windows/WindowView.jsx").windowProps} props
|
* @param {import("../../windows/WindowView.jsx").windowProps} props
|
||||||
*/
|
*/
|
||||||
export function Terminal({ setTitle, close: exit }) {
|
export function Terminal({ startPath, setTitle, close: exit }) {
|
||||||
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([]);
|
||||||
const virtualRoot = useVirtualRoot();
|
const virtualRoot = useVirtualRoot();
|
||||||
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate("~"));
|
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);
|
||||||
|
|
||||||
|
|
@ -24,7 +24,8 @@ export function Terminal({ setTitle, close: exit }) {
|
||||||
setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`);
|
setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`);
|
||||||
}, [currentDirectory.path, currentDirectory.root, setTitle]);
|
}, [currentDirectory.path, currentDirectory.root, setTitle]);
|
||||||
|
|
||||||
const prefix = `${USERNAME}@${HOSTNAME}:${currentDirectory.root ? "/" : currentDirectory.path}$ `;
|
const prefix = `${ANSI.fg.cyan + USERNAME}@${HOSTNAME + ANSI.reset}:`
|
||||||
|
+ `${ANSI.fg.blue + (currentDirectory.root ? "/" : currentDirectory.path) + ANSI.reset}$ `;
|
||||||
|
|
||||||
const updatedHistory = history;
|
const updatedHistory = history;
|
||||||
const pushHistory = (entry) => {
|
const pushHistory = (entry) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
.Terminal {
|
.Terminal {
|
||||||
--char-width: 0.626rem;
|
--char-width: 0.585rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
.Terminal * {
|
.Terminal * {
|
||||||
font-family: var(--mono-font-family);
|
font-family: var(--mono-font-family);
|
||||||
letter-spacing: 0.01rem;
|
letter-spacing: -0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Terminal p, .Terminal pre {
|
.Terminal p, .Terminal pre {
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.Prefix {
|
.Prefix {
|
||||||
|
display: flex;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +48,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin-left: var(--char-width);
|
/* margin-left: var(--char-width); */
|
||||||
}
|
}
|
||||||
|
|
||||||
.Input-container::after {
|
.Input-container::after {
|
||||||
|
|
@ -91,3 +92,30 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
cursor: text;
|
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-bright-black-fg { color: var(--dark-grey-d); }
|
||||||
|
.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-black-bg { background-color: var(--dark-grey-d); }
|
||||||
|
.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); }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
export const USERNAME = "user";
|
export const USERNAME = "user";
|
||||||
export const HOSTNAME = "prozilla-os";
|
export const HOSTNAME = "prozilla-os";
|
||||||
export const MAX_WIDTH = 50;
|
export const MAX_WIDTH = 50;
|
||||||
|
|
||||||
|
export const ANSI = {
|
||||||
|
fg: {
|
||||||
|
black: "\u001b[30m",
|
||||||
|
red: "\u001b[31m",
|
||||||
|
green: "\u001b[32m",
|
||||||
|
yellow: "\u001b[33m",
|
||||||
|
blue: "\u001b[34m",
|
||||||
|
magenta: "\u001b[35m",
|
||||||
|
cyan: "\u001b[36m",
|
||||||
|
white: "\u001b[37m",
|
||||||
|
},
|
||||||
|
bg: {
|
||||||
|
|
||||||
|
},
|
||||||
|
reset: "\u001b[0m",
|
||||||
|
};
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ANSI } from "./applications/terminal.js";
|
||||||
|
|
||||||
export const NAME = "ProzillaOS";
|
export const NAME = "ProzillaOS";
|
||||||
|
|
||||||
export const ASCII_LOGO = `
|
export const ASCII_LOGO = `
|
||||||
|
|
@ -16,3 +18,21 @@ export const ASCII_LOGO = `
|
||||||
...::---------------:.
|
...::---------------:.
|
||||||
.::---------::..
|
.::---------::..
|
||||||
....::... `;
|
....::... `;
|
||||||
|
|
||||||
|
export const ANSI_LOGO_COLOR = ANSI.fg.green;
|
||||||
|
export const ANSI_ASCII_LOGO = `
|
||||||
|
:.
|
||||||
|
-==.
|
||||||
|
.=====:
|
||||||
|
---::..:=======-.
|
||||||
|
:===+=----------::..
|
||||||
|
=+=---------------:..
|
||||||
|
--------------------:.
|
||||||
|
.:-+=----${ANSI.fg.white}*###*${ANSI_LOGO_COLOR}--${ANSI.fg.white}*####=${ANSI_LOGO_COLOR}---.
|
||||||
|
:==+----${ANSI.fg.white}#%+${ANSI_LOGO_COLOR}-${ANSI.fg.white}+%#${ANSI_LOGO_COLOR}-${ANSI.fg.white}##%*+${ANSI_LOGO_COLOR}----:.
|
||||||
|
.=----${ANSI.fg.white}#%+${ANSI_LOGO_COLOR}-${ANSI.fg.white}+%#${ANSI_LOGO_COLOR}-${ANSI.fg.white}*+${ANSI_LOGO_COLOR}-${ANSI.fg.white}%#+${ANSI_LOGO_COLOR}---:.
|
||||||
|
==----${ANSI.fg.white}*###*${ANSI_LOGO_COLOR}--${ANSI.fg.white}*###*${ANSI_LOGO_COLOR}----.
|
||||||
|
==+-------------------:.
|
||||||
|
...::---------------:.
|
||||||
|
.::---------::..
|
||||||
|
....::... `;
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import Command from "../command.js";
|
import Command from "../command.js";
|
||||||
|
|
||||||
export const cd = new Command("cd")
|
export const cd = new Command("cd")
|
||||||
.setRequireArgs(true)
|
|
||||||
.setManual({
|
.setManual({
|
||||||
purpose: "Change the current directory",
|
purpose: "Change the current directory",
|
||||||
usage: "cd path",
|
usage: "cd path",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ANSI } from "../../../../constants/applications/terminal.js";
|
||||||
import Command from "../command.js";
|
import Command from "../command.js";
|
||||||
|
|
||||||
export const ls = new Command("ls")
|
export const ls = new Command("ls")
|
||||||
|
|
@ -16,7 +17,7 @@ export const ls = new Command("ls")
|
||||||
if (!directory)
|
if (!directory)
|
||||||
return `${this.name}: Cannot access '${args[0]}': No such file or directory`;
|
return `${this.name}: Cannot access '${args[0]}': No such file or directory`;
|
||||||
|
|
||||||
const folderNames = directory.subFolders.map((folder) => folder.id);
|
const folderNames = directory.subFolders.map((folder) => `${ANSI.fg.blue}${folder.id}${ANSI.reset}`);
|
||||||
const fileNames = directory.files.map((file) => file.id);
|
const fileNames = directory.files.map((file) => file.id);
|
||||||
|
|
||||||
const contents = folderNames.concat(fileNames);
|
const contents = folderNames.concat(fileNames);
|
||||||
|
|
@ -24,5 +25,5 @@ export const ls = new Command("ls")
|
||||||
if (contents.length === 0)
|
if (contents.length === 0)
|
||||||
return { blank: true };
|
return { blank: true };
|
||||||
|
|
||||||
return contents.sort((nameA, nameB) => nameA.localeCompare(nameB)).join(" ");
|
return contents.sort().join(" ");
|
||||||
});
|
});
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { APPS } from "../../../../constants/applications.js";
|
import { APPS } from "../../../../constants/applications.js";
|
||||||
import { ASCII_LOGO, NAME } from "../../../../constants/branding.js";
|
import { ANSI } from "../../../../constants/applications/terminal.js";
|
||||||
|
import { ANSI_ASCII_LOGO, ANSI_LOGO_COLOR, NAME } from "../../../../constants/branding.js";
|
||||||
import { START_DATE } from "../../../../index.js";
|
import { START_DATE } from "../../../../index.js";
|
||||||
import { formatRelativeTime } from "../../../utils/date.js";
|
import { formatRelativeTime } from "../../../utils/date.js";
|
||||||
import AppsManager from "../../applications.js";
|
import AppsManager from "../../applications.js";
|
||||||
|
|
@ -10,7 +11,7 @@ export const neofetch = new Command("neofetch")
|
||||||
purpose: "Fetch system information"
|
purpose: "Fetch system information"
|
||||||
})
|
})
|
||||||
.setExecute((args, { username, hostname }) => {
|
.setExecute((args, { username, hostname }) => {
|
||||||
const leftColumn = ASCII_LOGO.split("\n");
|
const leftColumn = ANSI_ASCII_LOGO.split("\n");
|
||||||
const rightColumnWidth = username.length + hostname.length + 1;
|
const rightColumnWidth = username.length + hostname.length + 1;
|
||||||
|
|
||||||
const userAgent = navigator.userAgent;
|
const userAgent = navigator.userAgent;
|
||||||
|
|
@ -29,23 +30,27 @@ export const neofetch = new Command("neofetch")
|
||||||
browserName = "Unknown";
|
browserName = "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatLine = (label, text) => ANSI.fg.green + label.toUpperCase() + ANSI.reset + ": " + text;
|
||||||
|
|
||||||
const rightColumn = [
|
const rightColumn = [
|
||||||
`${username}@${hostname}`,
|
`${ANSI.fg.green + username + ANSI.reset}@${ANSI.fg.green + hostname + ANSI.reset}`,
|
||||||
"-".repeat(rightColumnWidth),
|
"-".repeat(rightColumnWidth),
|
||||||
`OS: ${NAME}`,
|
formatLine("os", NAME),
|
||||||
`UPTIME: ${formatRelativeTime(START_DATE, 2, false)}`,
|
formatLine("uptime", formatRelativeTime(START_DATE, 2, false)),
|
||||||
`RESOLUTION: ${window.innerWidth}x${window.innerHeight}`,
|
formatLine("resolution", window.innerWidth + "x" + window.innerHeight),
|
||||||
"THEME: default",
|
formatLine("theme", "default"),
|
||||||
"ICONS: Font Awesome",
|
formatLine("icons", "Font Awesome"),
|
||||||
`TERMINAL: ${AppsManager.getApp(APPS.TERMINAL)?.name ?? "Unknown"}`,
|
formatLine("terminal", AppsManager.getApp(APPS.TERMINAL)?.name ?? "Unknown"),
|
||||||
`BROWSER: ${browserName}`,
|
formatLine("browser", browserName),
|
||||||
`PLATFORM: ${navigator.platform}`,
|
formatLine("platform", navigator.platform),
|
||||||
`LANGUAGE: ${navigator.language}`,
|
formatLine("language", navigator.language),
|
||||||
|
"",
|
||||||
|
Object.values(ANSI.fg).map((colorCode) => colorCode + "███").join("") + ANSI.reset,
|
||||||
];
|
];
|
||||||
|
|
||||||
const combined = [];
|
const combined = [];
|
||||||
for (let i = 1; i < leftColumn.length; i++) {
|
for (let i = 1; i < leftColumn.length; i++) {
|
||||||
let line = `${leftColumn[i]} `;
|
let line = `${ANSI_LOGO_COLOR + leftColumn[i] + ANSI.reset} `;
|
||||||
|
|
||||||
if (i <= rightColumn.length) {
|
if (i <= rightColumn.length) {
|
||||||
line += rightColumn[i - 1];
|
line += rightColumn[i - 1];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
:root {
|
:root {
|
||||||
|
--white-a: #fff;
|
||||||
--pink-a: #ff9ff3;
|
--pink-a: #ff9ff3;
|
||||||
--pink-b: #f368e0;
|
--pink-b: #f368e0;
|
||||||
--yellow-a: #feca57;
|
--yellow-a: #feca57;
|
||||||
|
|
@ -24,7 +25,7 @@
|
||||||
--dark-grey-d: hsl(212, 14%, 10%);
|
--dark-grey-d: hsl(212, 14%, 10%);
|
||||||
--dark-grey-e: hsl(212, 12%, 8%);
|
--dark-grey-e: hsl(212, 12%, 8%);
|
||||||
|
|
||||||
--foreground-color-a: #fff;
|
--foreground-color-a: var(--white-a);
|
||||||
--foreground-color-b: var(--grey-a);
|
--foreground-color-b: var(--grey-a);
|
||||||
--foreground-color-c: var(--grey-b);
|
--foreground-color-c: var(--grey-b);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue