Skip to content

Commit 885fa7c

Browse files
committed
terminal: Made cmatrix adapt to size
1 parent e20647f commit 885fa7c

31 files changed

Lines changed: 149 additions & 122 deletions

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@
5151
"pnpm": "^9.15.9"
5252
},
5353
"devDependencies": {
54-
"@prozilla-os/dev-tools": "workspace:*",
5554
"@changesets/cli": "^2.29.8",
5655
"@eslint/js": "^9.39.2",
56+
"@prozilla-os/dev-tools": "workspace:*",
5757
"@types/eslint": "^8.56.12",
5858
"@types/gh-pages": "^6.1.0",
5959
"@types/node": "^20.19.30",
@@ -81,7 +81,7 @@
8181
"last 1 safari version"
8282
]
8383
},
84-
"packageManager": "pnpm@10.29.3",
84+
"packageManager": "pnpm@10.33.0",
8585
"pnpm": {
8686
"overrides": {
8787
"prozilla-os": "workspace:*",

packages/apps/terminal/src/components/Terminal.tsx

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { MouseEventHandler, MutableRefObject, useEffect, useRef, useState } from
22
import styles from "./Terminal.module.css";
33
import { OutputLine } from "./OutputLine";
44
import { InputLine } from "./InputLine";
5-
import { App, SettingsManager, useSettingsManager, useSystemManager, useVirtualRoot, VirtualFolder, WindowProps } from "@prozilla-os/core";
5+
import { useSettingsManager, useSystemManager, useVirtualRoot, VirtualFolder, WindowProps } from "@prozilla-os/core";
66
import { HOSTNAME, USERNAME, WELCOME_MESSAGE } from "../constants/terminal.const";
77
import { Stream } from "../core/stream";
88
import { CommandResponse } from "../core/command";
99
import { formatError } from "../core/_utils/terminal.utils";
1010
import { CommandsManager } from "../core/commands";
11-
import { ANSI, clamp, removeFromArray } from "@prozilla-os/shared";
11+
import { Ansi, ANSI, clamp, removeFromArray, Vector2 } from "@prozilla-os/shared";
1212

1313
export interface TerminalProps extends WindowProps {
1414
path?: string;
@@ -32,13 +32,14 @@ export function Terminal({ app, path: startPath, input, setTitle, close: exit, a
3232
}]);
3333
const virtualRoot = useVirtualRoot();
3434
const [currentDirectory, setCurrentDirectory] = useState<VirtualFolder>(virtualRoot?.navigate(startPath ?? "~") as VirtualFolder);
35-
const inputRef = useRef(null);
35+
const inputRef = useRef<HTMLInputElement>(null);
3636
const [historyIndex, setHistoryIndex] = useState(0);
3737
const [stream, setStream] = useState<Stream | null>(null);
3838
const [streamOutput, setStreamOutput] = useState<string | null>(null);
39-
const ref = useRef(null);
39+
const ref = useRef<HTMLDivElement>(null);
4040
const [streamFocused, setStreamFocused] = useState(false);
4141
const settingsManager = useSettingsManager();
42+
const sizeRef = useRef(Vector2.ZERO);
4243

4344
useEffect(() => {
4445
if (currentDirectory != null)
@@ -49,11 +50,12 @@ export function Terminal({ app, path: startPath, input, setTitle, close: exit, a
4950
if (!inputRef.current || !active)
5051
return;
5152

52-
(inputRef.current as unknown as HTMLInputElement).focus();
53+
inputRef.current.focus();
5354
}, [inputRef, active]);
5455

5556
const scrollDown = () => {
56-
(ref.current as unknown as HTMLDivElement).scrollTop = (ref.current as unknown as HTMLDivElement).scrollHeight;
57+
if (ref.current)
58+
ref.current.scrollTop = ref.current.scrollHeight;
5759
};
5860

5961
useEffect(() => {
@@ -71,8 +73,33 @@ export function Terminal({ app, path: startPath, input, setTitle, close: exit, a
7173
scrollDown();
7274
}, [inputValue]);
7375

74-
const prefix = `${ANSI.fg.cyan + USERNAME}@${HOSTNAME + ANSI.reset}:`
75-
+ `${ANSI.fg.blue + ((currentDirectory?.root || currentDirectory == null) ? "/" : currentDirectory?.path) + ANSI.reset}$ `;
76+
useEffect(() => {
77+
if (!ref.current) return;
78+
79+
const measure = () => {
80+
if (!ref.current) return;
81+
82+
const style = getComputedStyle(ref.current);
83+
const fontSize = parseFloat(style.fontSize);
84+
const charWidth = 0.585 * fontSize;
85+
const charHeight = 1.25 * fontSize;
86+
const { width, height } = ref.current.getBoundingClientRect();
87+
88+
sizeRef.current.set(
89+
Math.ceil(width / charWidth),
90+
Math.ceil(height / charHeight)
91+
);
92+
};
93+
94+
const observer = new ResizeObserver(measure);
95+
observer.observe(ref.current);
96+
measure();
97+
98+
return () => observer.disconnect();
99+
}, [ref]);
100+
101+
const prefix = Ansi.cyan(`${USERNAME}@${HOSTNAME}`) + ":"
102+
+ Ansi.blue(`${((currentDirectory?.root || currentDirectory == null) ? "/" : currentDirectory?.path)}`) + "$ ";
76103

77104
const updatedHistory = history;
78105
const pushHistory = (entry: HistoryEntry) => {
@@ -100,7 +127,7 @@ export function Terminal({ app, path: startPath, input, setTitle, close: exit, a
100127
let lastOutput: CommandResponse | null = null;
101128

102129
stream.onAsync(Stream.SEND_EVENT, async (text) => {
103-
let output: CommandResponse = text as CommandResponse;
130+
let output: CommandResponse = text;
104131

105132
for (const pipe of pipes) {
106133
if (output instanceof Stream)
@@ -110,7 +137,7 @@ export function Terminal({ app, path: startPath, input, setTitle, close: exit, a
110137
output = await handleInput(output ? `${pipe} ${output as string}` : pipe);
111138
}
112139

113-
if ((output as unknown) instanceof Stream) {
140+
if (output instanceof Stream) {
114141
stream.stop();
115142
promptOutput(ANSI.fg.red + "Stream failed");
116143
return;
@@ -214,9 +241,10 @@ export function Terminal({ app, path: startPath, input, setTitle, close: exit, a
214241
exit,
215242
inputs,
216243
timestamp,
217-
settingsManager: settingsManager as SettingsManager,
244+
settingsManager: settingsManager!,
218245
systemManager,
219-
app: app as App,
246+
app: app!,
247+
size: sizeRef.current,
220248
});
221249

222250
if (response == null)
@@ -354,7 +382,7 @@ export function Terminal({ app, path: startPath, input, setTitle, close: exit, a
354382
onClick={(event) => {
355383
if (window.getSelection()?.toString() === "") {
356384
event.preventDefault();
357-
(inputRef.current as HTMLInputElement | null)?.focus();
385+
(inputRef.current)?.focus();
358386
}
359387
}}
360388
>

packages/apps/terminal/src/core/command.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { App, SettingsManager, SystemManager, VirtualFolder, VirtualRoot } from "@prozilla-os/core";
1+
import { App, SettingsManager, SystemManager, Vector2, VirtualFolder, VirtualRoot } from "@prozilla-os/core";
22
import { Stream } from "./stream";
33
import { Dispatch, SetStateAction } from "react";
44
import { HistoryEntry } from "../components/Terminal";
@@ -26,9 +26,14 @@ export type ExecuteParams = {
2626
settingsManager: SettingsManager,
2727
systemManager: SystemManager,
2828
app: App,
29+
readonly size: Vector2
2930
};
3031

31-
type Execute = (args?: string[], params?: ExecuteParams) => CommandResponse | Promise<CommandResponse>;
32+
type Execute = (
33+
((args: string[], params: ExecuteParams) => CommandResponse | Promise<CommandResponse>)
34+
| ((args: string[]) => CommandResponse | Promise<CommandResponse>)
35+
| (() => CommandResponse | Promise<CommandResponse>)
36+
);
3237

3338
type Manual = {
3439
purpose?: string,

packages/apps/terminal/src/core/commands/cat.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { VirtualFile } from "@prozilla-os/core";
22
import { formatError } from "../_utils/terminal.utils";
3-
import { Command, ExecuteParams } from "../command";
3+
import { Command } from "../command";
44

55
export const cat = new Command()
66
.setRequireArgs(true)
@@ -9,9 +9,8 @@ export const cat = new Command()
99
usage: "cat [options] [files]",
1010
description: "Concetenate files to standard output.",
1111
})
12-
.setExecute(function(this: Command, args, params) {
13-
const { currentDirectory, options } = params as ExecuteParams;
14-
const fileId = (args as string[])[0];
12+
.setExecute(function(this: Command, args, { currentDirectory, options }) {
13+
const fileId = args[0];
1514
const { name, extension } = VirtualFile.splitId(fileId);
1615
const file = currentDirectory.findFile(name, extension);
1716

packages/apps/terminal/src/core/commands/cd.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { VirtualFile, VirtualFolder } from "@prozilla-os/core";
22
import { formatError } from "../_utils/terminal.utils";
3-
import { Command, ExecuteParams } from "../command";
3+
import { Command } from "../command";
44

55
export const cd = new Command()
66
.setManual({
77
purpose: "Change the current directory",
88
usage: "cd [PATH]",
99
description: "Change working directory to given path (the home directory by default).",
1010
})
11-
.setExecute(function(this: Command, args, params) {
12-
const { currentDirectory, setCurrentDirectory } = params as ExecuteParams;
13-
const path = (args as string[])[0] ?? "~";
11+
.setExecute(function(this: Command, args, { currentDirectory, setCurrentDirectory }) {
12+
const path = args[0] ?? "~";
1413
let destination = currentDirectory.navigate(path);
1514

1615
if (!destination)
17-
return formatError(this.name, `${(args as string[])[0]}: No such file or directory`);
16+
return formatError(this.name, `${(args)[0]}: No such file or directory`);
1817

1918
if (destination instanceof VirtualFile)
2019
destination = destination.parent as VirtualFolder;

packages/apps/terminal/src/core/commands/clear.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { Command, ExecuteParams } from "../command";
1+
import { Command } from "../command";
22

33
export const clear = new Command()
44
.setManual({
55
purpose: "Clear terminal screen",
66
})
7-
.setExecute(function(_args, params) {
8-
const { pushHistory } = params as ExecuteParams;
7+
.setExecute(function(_args, { pushHistory }) {
98
pushHistory?.({
109
clear: true,
1110
isInput: false,

packages/apps/terminal/src/core/commands/cmatrix.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import { Command } from "../command";
44
import { Stream } from "../stream";
55

66
const ANIMATION_SPEED = 1.25;
7-
const SCREEN_WIDTH = 75;
8-
const SCREEN_HEIGHT = 20;
97
const CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.*\\/()#@&$!?%°:<>[]";
108

119
const PARTICLES = {
12-
framesBetweenSpawn: 30,
10+
spawnRate: 30,
1311
fallSpeed: 1,
1412
minSize: 5,
1513
maxSize: 25,
@@ -20,11 +18,25 @@ type Particle = {
2018
size: number;
2119
};
2220

23-
function generateScreen(frame: number, screen: string[][], particles: Particle[]): string {
21+
function initializeScreen(size: Vector2) {
22+
const screen: string[][] = [];
23+
for (let y = 0; y < size.y; y++) {
24+
const row: string[] = [];
25+
for (let x = 0; x < size.x; x++) {
26+
row.push(" ");
27+
}
28+
screen.push(row);
29+
}
30+
return screen;
31+
}
32+
33+
function generateScreen(frame: number, screen: string[][], particles: Particle[], size: Vector2): string {
34+
const framesBetweenSpawn = Math.round(5000 / (PARTICLES.spawnRate * size.x));
35+
2436
// Spawn new particles
25-
if (frame % PARTICLES.framesBetweenSpawn) {
37+
if (framesBetweenSpawn === 0 || frame % framesBetweenSpawn === 0) {
2638
const newParticle: Particle = {
27-
position: new Vector2(randomRange(0, SCREEN_WIDTH), SCREEN_HEIGHT).round(),
39+
position: new Vector2(randomRange(0, size.x - 1), size.y).round(),
2840
size: Math.round(randomRange(PARTICLES.minSize, PARTICLES.maxSize)),
2941
};
3042
particles.push(newParticle);
@@ -39,12 +51,12 @@ function generateScreen(frame: number, screen: string[][], particles: Particle[]
3951
const positionX = particle.position.x;
4052
const positionY = particle.position.y + i + particle.size;
4153

42-
if (positionY < SCREEN_HEIGHT && positionY >= 0)
54+
if (positionY < size.y && positionY >= 0)
4355
screen[positionY][positionX] = " ";
4456
}
4557

4658
// Remove offscreen particles
47-
if (particle.position.y + particle.size <= 0 || particle.position.x >= SCREEN_WIDTH)
59+
if (particle.position.y + particle.size <= 0 || particle.position.x >= size.x)
4860
return removeFromArray(particle, particles);
4961

5062
for (let i = 0; i < particle.size; i++) {
@@ -57,7 +69,7 @@ function generateScreen(frame: number, screen: string[][], particles: Particle[]
5769
const positionX = particle.position.x;
5870
const positionY = particle.position.y + i;
5971

60-
if (positionX < SCREEN_WIDTH && positionY < SCREEN_HEIGHT && positionY >= 0) {
72+
if (positionX < size.x && positionY < size.y && positionY >= 0) {
6173
screen[positionY][positionX] = color + character + ANSI.reset;
6274
}
6375
}
@@ -71,23 +83,20 @@ export const cmatrix = new Command()
7183
purpose: "Show a scrolling 'Matrix' like screen",
7284
usage: "cmatrix",
7385
})
74-
.setExecute(function() {
86+
.setExecute(function(_args, { size }) {
7587
const stream = new Stream();
7688
const particles: Particle[] = [];
7789

7890
// Create screen
79-
const screen: string[][] = [];
80-
for (let y = 0; y < SCREEN_HEIGHT; y++) {
81-
const row: string[] = [];
82-
for (let x = 0; x < SCREEN_WIDTH; x++) {
83-
row.push(" ");
84-
}
85-
screen.push(row);
86-
}
91+
let screen = initializeScreen(size);
8792

8893
let frame = 0;
8994
const interval = setInterval(() => {
90-
const text = generateScreen(frame, screen, particles);
95+
if (screen.length != size.y || (screen.length != 0 ? screen[0].length : 0) != size.x) {
96+
screen = initializeScreen(size);
97+
}
98+
99+
const text = generateScreen(frame, screen, particles, size);
91100
stream.send(text);
92101
frame++;
93102
}, 100 / ANIMATION_SPEED);

packages/apps/terminal/src/core/commands/compgen.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { Command, ExecuteParams } from "../command";
1+
import { Command } from "../command";
22
import { CommandsManager } from "../commands";
33

44
export const compgen = new Command()
55
.setManual({
66
purpose: "Display a list of all commands",
77
})
88
.setRequireOptions(true)
9-
.setExecute(function(_args, params) {
10-
const { options } = params as ExecuteParams;
9+
.setExecute(function(_args, { options }) {
1110
if (options?.includes("c")) {
1211
return CommandsManager.COMMANDS.map((command) => command.name).sort().join("\n");
1312
}

packages/apps/terminal/src/core/commands/cowsay.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { MAX_WIDTH } from "../../constants/terminal.const";
2-
import { Command, ExecuteParams } from "../command";
2+
import { Command } from "../command";
33

44
const COW = `
55
\\ ^__^
@@ -15,8 +15,7 @@ export const cowsay = new Command()
1515
usage: "cowsay text",
1616
description: "Show ASCII art of a cow saying something.",
1717
})
18-
.setExecute(function(_args, params) {
19-
const { rawInputValue } = params as ExecuteParams;
18+
.setExecute(function(_args, { rawInputValue }) {
2019

2120
// Separate input value into lines
2221
const segments = rawInputValue?.split(" ");

packages/apps/terminal/src/core/commands/dir.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { Command, ExecuteParams } from "../command";
1+
import { Command } from "../command";
22

33
export const dir = new Command()
44
.setManual({
55
purpose: "List all directories in the current directory",
66
})
7-
.setExecute(function(_args, params) {
8-
const { currentDirectory } = params as ExecuteParams;
7+
.setExecute(function(_args, { currentDirectory }) {
98
const folderNames = currentDirectory.subFolders.map((folder) => folder.id);
109

1110
if (folderNames.length === 0)

0 commit comments

Comments
 (0)