diff --git a/src/components/applications/terminal/Terminal.jsx b/src/components/applications/terminal/Terminal.jsx
index 96171fa..b93a9f7 100644
--- a/src/components/applications/terminal/Terminal.jsx
+++ b/src/components/applications/terminal/Terminal.jsx
@@ -1,7 +1,8 @@
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import styles from "./Terminal.module.css";
import { useVirtualRoot } from "../../../hooks/virtual-drive/VirtualRootContext.js";
import { Command } from "../../../features/applications/terminal/commands.js";
+import { clamp } from "../../../features/math/clamp.js";
const USERNAME = "user";
const HOSTNAME = "prozilla-os";
@@ -27,22 +28,45 @@ function OutputLine({ text }) {
* @param {Function} props.onChange
* @param {Function} props.onKeyUp
* @param {Function} props.onKeyDown
+ * @param {import("react").MutableRefObject} props.inputRef
*/
-function InputLine({ value, prefix, onChange, onKeyUp, onKeyDown }) {
+function InputLine({ value, prefix, onChange, onKeyUp, onKeyDown, inputRef }) {
+ const [cursorPosition, setCursorPosition] = useState(0);
+
+ const checkCursorPosition = () => {
+ setCursorPosition(inputRef.current?.selectionStart);
+ };
+
return (
{prefix && {prefix}
}
-
-
+
+ {value}
+ {
+ onChange(event);
+ checkCursorPosition();
+ }}
+ ref={inputRef}
+ onKeyUp={onKeyUp}
+ onKeyDown={(event) => {
+ onKeyDown(event);
+ checkCursorPosition();
+ }}
+ onClick={checkCursorPosition}
+ onTouchEnd={checkCursorPosition}
+ onSelect={checkCursorPosition}
+ onCut={checkCursorPosition}
+ onCopy={checkCursorPosition}
+ onPaste={checkCursorPosition}
+ spellCheck={false}
+ autoComplete="off"
+ autoFocus
+ size=""
+ />
+
);
}
@@ -53,6 +77,8 @@ export function Terminal({ setTitle }) {
const [history, setHistory] = useState([]);
const virtualRoot = useVirtualRoot();
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate("~"));
+ const inputRef = useRef(null);
+ const [historyIndex, setHistoryIndex] = useState(0);
useEffect(() => {
setTitle(`${USERNAME}@${HOSTNAME}: ${currentDirectory.root ? "/" : currentDirectory.path}`);
@@ -76,7 +102,8 @@ export function Terminal({ setTitle }) {
const submitInput = (value) => {
pushHistory({
text: prefix + value,
- isInput: true
+ isInput: true,
+ value
});
setInputValue("");
@@ -119,13 +146,38 @@ export function Terminal({ setTitle }) {
}
};
+ const updateHistoryIndex = (delta) => {
+ const inputHistory = history.filter(({ isInput }) => isInput);
+ const index = clamp(historyIndex + delta, 0, inputHistory.length);
+
+ if (index === historyIndex) {
+ if (delta < 0) {
+ setInputValue("");
+ }
+
+ return;
+ }
+
+ if (index === 0) {
+ setInputValue("");
+ } else {
+ setInputValue(inputHistory[inputHistory.length - index].value);
+ }
+
+ setHistoryIndex(index);
+ };
+
const onKeyDown = (event) => {
const value = event.target.value;
+ const { key } = event;
- // console.log(event);
- if (event.key === "Enter") {
+ if (key === "Enter") {
submitInput(value);
setInputKey((previousKey) => previousKey + 1);
+ } else if (key === "ArrowUp") {
+ updateHistoryIndex(1);
+ } else if (key === "ArrowDown") {
+ updateHistoryIndex(-1);
}
};
@@ -169,6 +221,12 @@ export function Terminal({ setTitle }) {
className={styles.Terminal}
onMouseDown={onMouseDown}
onContextMenu={onContextMenu}
+ onClick={(event) => {
+ if (window.getSelection().toString() === "") {
+ event.preventDefault();
+ inputRef.current?.focus();
+ }
+ }}
>
{displayHistory()}
);
diff --git a/src/components/applications/terminal/Terminal.module.css b/src/components/applications/terminal/Terminal.module.css
index 923fb1d..d984352 100644
--- a/src/components/applications/terminal/Terminal.module.css
+++ b/src/components/applications/terminal/Terminal.module.css
@@ -1,13 +1,18 @@
.Terminal {
+ --char-width: 0.626rem;
+
display: flex;
flex-direction: column;
+ align-items: flex-start;
padding: 0.5rem;
- overflow-y: auto;
+ overflow: auto;
height: 100%;
+ cursor: text;
}
.Terminal * {
font-family: var(--mono-font-family);
+ letter-spacing: 0.01rem;
}
.Terminal p {
@@ -27,21 +32,54 @@
line-height: 1.25rem;
font-size: 1rem;
text-align: start;
- white-space: break-spaces;
+ white-space: pre;
}
.Input {
height: 1.25rem;
}
-.Input input {
- width: 100%;
+.Input-container {
+ --cursor-width: var(--char-width);
+ --cursor-offset: 0;
+
+ position: relative;
height: 100%;
+ width: fit-content;
+ margin-left: var(--char-width);
+}
+
+.Input-container::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: calc(var(--cursor-offset) * var(--char-width) * 0.9746124950079872);
+ width: var(--cursor-width);
+ height: 100%;
+ background-color: var(--foreground-color-a);
+ animation: blink 1000ms step-end infinite;
+}
+
+.Input-container input {
+ opacity: 0;
+ position: absolute;
+ left: 0;
+ width: 100%;
+ padding: 0;
background: none;
border: none;
outline: none;
font-size: inherit;
- margin-left: 0.45rem;
+ caret-color: transparent;
+}
+
+@keyframes blink {
+ from, to {
+ background-color: transparent;
+ }
+ 50% {
+ background-color: var(--foreground-color-a);
+ }
}
.Input label {
diff --git a/src/components/windows/WindowView.jsx b/src/components/windows/WindowView.jsx
index 37a739c..88240b9 100644
--- a/src/components/windows/WindowView.jsx
+++ b/src/components/windows/WindowView.jsx
@@ -22,8 +22,9 @@ import { useContextMenu } from "../../hooks/modals/ContextMenu.js";
* @param {boolean} props.focused
* @param {Function} props.onInteract
* @param {object} props.options
+ * @param {boolean} props.active
*/
-export const WindowView = memo(function Window({ id, app, size, position, focused = false, onInteract, options }) {
+export const WindowView = memo(function Window({ id, app, size, position, onInteract, options, active }) {
const windowsManager = useWindowsManager();
const nodeRef = useRef(null);
const [modalsManager, modals] = useModals();
@@ -161,7 +162,7 @@ export const WindowView = memo(function Window({ id, app, size, position, focuse
diff --git a/src/components/windows/WindowsView.jsx b/src/components/windows/WindowsView.jsx
index 70256ea..0cdbcf8 100644
--- a/src/components/windows/WindowsView.jsx
+++ b/src/components/windows/WindowsView.jsx
@@ -17,9 +17,10 @@ export function WindowsView() {
// TO DO: prevent windows from being rerendered when order is changed
return (
- {sortedWindows.map(({ id, app, size, position, options }) =>
+ {sortedWindows.map(({ id, app, size, position, options }, index) =>
{ windowsManager.focus(id); }}
+ active={index === 0}
id={id}
key={id}
app={app}
diff --git a/src/features/applications/terminal/commands.js b/src/features/applications/terminal/commands.js
index c71a97d..f2e48a2 100644
--- a/src/features/applications/terminal/commands.js
+++ b/src/features/applications/terminal/commands.js
@@ -2,23 +2,22 @@ import { START_DATE } from "../../../index.js";
import { formatRelativeTime } from "../../utils/date.js";
import ApplicationsManager from "../applications.js";
-export const ASCII_LOGO = `
+export const ASCII_LOGO = `
:.
-==.
.=====:
---::..:=======-.
:===+=----------::..
=+=---------------:..
- --------------=-----:.
- .:-+=---=*#*#*==*#*##=---.
- :==+---=#%+=+%#+##**+=---:.
- .=---=#%+=+%*+*++*%+---:.
- ==---=*###*==*###*=---.
+ --------------------:.
+ .:-+=----*###*--*####=---.
+ :==+----#%+-+%#-##%*+----:.
+ .=----#%+-+%#-*+-%#+---:.
+ ==----*###*--*###*----.
==+-------------------:.
...::---------------:.
.::---------::..
- ....::...
-`;
+ ....::... `;
export class Command {
/**
@@ -149,9 +148,10 @@ export class Command {
}),
new Command("neofetch", (args, { username, hostname }) => {
const leftColumn = ASCII_LOGO.split("\n");
+ const rightColumnWidth = username.length + hostname.length + 1;
const rightColumn = [
`${username}@${hostname}`,
- "-".repeat(username.length + hostname.length + 1),
+ "-".repeat(rightColumnWidth),
"OS: ProzillaOS",
`UPTIME: ${formatRelativeTime(START_DATE, 2, false)}`,
`RESOLUTION: ${window.innerWidth}x${window.innerHeight}`,
@@ -161,11 +161,14 @@ export class Command {
];
const combined = [];
- for (let i = 1; i < leftColumn.length - 1; i++) {
+ for (let i = 1; i < leftColumn.length; i++) {
let line = `${leftColumn[i]} `;
if (i <= rightColumn.length) {
line += rightColumn[i - 1];
+ } else {
+ // This fixes a weird display bug on Safari mobile
+ line += " ".repeat(rightColumnWidth);
}
combined.push(line);
diff --git a/src/styles/global.css b/src/styles/global.css
index af61bc5..bf8a3ff 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -105,7 +105,8 @@ code {
}
*::-webkit-scrollbar {
- width: 20px;
+ width: 1.25rem;
+ height: 1.25rem;
}
*::-webkit-scrollbar-track {
@@ -120,4 +121,8 @@ code {
backdrop-filter: invert(100%);
transition: 200ms ease-in-out;
z-index: 1;
+}
+
+*::-webkit-scrollbar-corner {
+ background-color: transparent;
}
\ No newline at end of file