Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
9970c58e2b | |||
ee7b2168d9 |
4
bun.lock
4
bun.lock
@ -10,7 +10,7 @@
|
|||||||
"astro": "^5.2.5",
|
"astro": "^5.2.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"sass": "^1.83.4",
|
"sass": "^1.84.0",
|
||||||
"scss": "^0.2.4",
|
"scss": "^0.2.4",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -722,7 +722,7 @@
|
|||||||
|
|
||||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
|
|
||||||
"sass": ["sass@1.83.4", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA=="],
|
"sass": ["sass@1.84.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
|
"scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
"astro": "^5.2.5",
|
"astro": "^5.2.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"sass": "^1.83.4",
|
"sass": "^1.84.0",
|
||||||
"scss": "^0.2.4"
|
"scss": "^0.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,6 +16,10 @@ const {href, display, color = "transparent"} = Astro.props
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 20px 0 20px;
|
padding: 0 20px 0 20px;
|
||||||
|
|
||||||
&:hover { background-color: var(--hf-button-hover-color) !important }
|
&:hover {
|
||||||
|
background-color: var(--hf-button-hover-color) !important;
|
||||||
|
|
||||||
|
& > a { color: black }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -2,6 +2,7 @@ const red = (s: string) => <span className="red">{s}</span>
|
|||||||
const green = (s: string) => <span className="green">{s}</span>
|
const green = (s: string) => <span className="green">{s}</span>
|
||||||
const blue = (s: string) => <span className="blue">{s}</span>
|
const blue = (s: string) => <span className="blue">{s}</span>
|
||||||
const cyan = (s: string) => <span className="cyan">{s}</span>
|
const cyan = (s: string) => <span className="cyan">{s}</span>
|
||||||
|
const bold = (s: string) => <span className="bold">{s}</span>
|
||||||
|
|
||||||
export default function rgb(s: string, Ru8: number, Gu8: number, Bu8: number) {
|
export default function rgb(s: string, Ru8: number, Gu8: number, Bu8: number) {
|
||||||
return <span style={{color: `rgb(${Ru8},${Gu8},${Bu8})`}}>{s}</span>
|
return <span style={{color: `rgb(${Ru8},${Gu8},${Bu8})`}}>{s}</span>
|
||||||
@ -11,5 +12,6 @@ export {
|
|||||||
red,
|
red,
|
||||||
green,
|
green,
|
||||||
blue,
|
blue,
|
||||||
cyan
|
cyan,
|
||||||
|
bold
|
||||||
}
|
}
|
@ -1,26 +0,0 @@
|
|||||||
import { working_dir } from "../fs"
|
|
||||||
|
|
||||||
type args = string[]
|
|
||||||
|
|
||||||
function ls(args: args) {
|
|
||||||
console.log(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
function pwd(args: args) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function cat(args: args) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
interface commands_list {
|
|
||||||
[index: string]: (args: args) => void
|
|
||||||
}
|
|
||||||
const commands: commands_list = {
|
|
||||||
["ls"]: ls,
|
|
||||||
["pwd"]: pwd,
|
|
||||||
["cat"]: cat,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default commands
|
|
53
src/components/react/shell/command/list.tsx
Normal file
53
src/components/react/shell/command/list.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { JSX } from "react"
|
||||||
|
import { bold } from "../color"
|
||||||
|
import { get_working_dir_name_full, set_working_dir, SetDirStatus } from "../fs/fn"
|
||||||
|
|
||||||
|
type args = string[]
|
||||||
|
type command = JSX.Element | boolean
|
||||||
|
|
||||||
|
function parse_ls(entries: JSX.Element[]) {
|
||||||
|
return <div className="horizontal-display">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function cd(args: args): command {
|
||||||
|
const new_dir_status = set_working_dir(args[1])
|
||||||
|
|
||||||
|
if (new_dir_status === SetDirStatus.NotADirectory) {
|
||||||
|
return <p>{"cd: \""}{bold(args[1])}{"\" is not a directory"}</p>
|
||||||
|
} else if (new_dir_status === SetDirStatus.NotFound) {
|
||||||
|
return <p>{"cd: The directory \""}{bold(args[1])}{"\" does not exist"}</p>
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function ls(args: args): command {
|
||||||
|
// if (args[1] === undefined) {
|
||||||
|
// for (const dir_name in working_dir) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// return <p>{`${working_dir}`}</p>
|
||||||
|
// }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function pwd(args: args): command {
|
||||||
|
return <p>{`${get_working_dir_name_full()}`}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
function cat(args: args): command {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
interface commands_list {
|
||||||
|
[index: string]: (args: args) => command
|
||||||
|
}
|
||||||
|
const commands: commands_list = {
|
||||||
|
["cd"]: cd,
|
||||||
|
["ls"]: ls,
|
||||||
|
["pwd"]: pwd,
|
||||||
|
["cat"]: cat,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default commands
|
@ -1,14 +1,10 @@
|
|||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
import type { Root } from "react-dom/client";
|
|
||||||
import { type newElement } from "../../terminal/exec";
|
|
||||||
|
|
||||||
import commands from "./list";
|
import commands from "./list";
|
||||||
|
import { bold } from "../color";
|
||||||
type Renderer<T> = React.Dispatch<React.SetStateAction<T>>
|
|
||||||
|
|
||||||
function trim(stdin: string): string {
|
function trim(stdin: string): string {
|
||||||
const trimmed_str: string[] = []
|
const trimmed_str: string[] = []
|
||||||
stdin.split(" ").forEach(s => { if (s != "") { trimmed_str.push(s) } })
|
stdin.split(" ").forEach(s => { if (s !== "") { trimmed_str.push(s) } })
|
||||||
return trimmed_str.join(" ")
|
return trimmed_str.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,26 +12,26 @@ function to_args(trimmed_str: string): string[] {
|
|||||||
return trimmed_str.split(" ")
|
return trimmed_str.split(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
function valid_command(args: string[]): boolean {
|
function valid_command(args: string[]): JSX.Element | undefined {
|
||||||
for (const command_in_list in commands) {
|
for (const command_in_list in commands) {
|
||||||
const command = args[0]
|
const command = args[0]
|
||||||
if (command === command_in_list) {
|
if (command === command_in_list) {
|
||||||
commands[command_in_list](args)
|
return commands[command_in_list](args)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
function unknown_command(name: string) {
|
function unknown_command(cmd_name: string) {
|
||||||
return <p>{`sh: Unknown command: ${name}`}</p>
|
return <p>{"shell: Unknown command: "}{bold(cmd_name)}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function run(stdin: string) {
|
export default function run(stdin: string) {
|
||||||
const args = to_args(trim(stdin))
|
const args = to_args(trim(stdin))
|
||||||
|
const command = valid_command(args)
|
||||||
|
|
||||||
if (args[0] !== "" && !valid_command(args)) {
|
if (args[0] !== "" && !command) {
|
||||||
return unknown_command(args[0])
|
return unknown_command(args[0])
|
||||||
}
|
}
|
||||||
return <></>
|
return command ? command : <></>
|
||||||
}
|
}
|
@ -33,14 +33,14 @@ function new_prompt(): JSX.Element {
|
|||||||
return display_prompt()
|
return display_prompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyboard_events(terminal_window: HTMLElement, new_element_f: newElement) {
|
function keyboard_events(terminal_window: HTMLElement, new_elements_f: newElement) {
|
||||||
const terminal_event = (keyboard_event: KeyboardEvent) => {
|
const terminal_event = (keyboard_event: KeyboardEvent) => {
|
||||||
if (keyboard_event.key === Key.Enter) {
|
if (keyboard_event.key === Key.Enter) {
|
||||||
const current_prompt = get_current_prompt()
|
const current_prompt = get_current_prompt()
|
||||||
if (current_prompt) {
|
if (current_prompt) {
|
||||||
const prompt = new_prompt()
|
const prompt = new_prompt()
|
||||||
const output = run(current_prompt.value)
|
const output = run(current_prompt.value)
|
||||||
new_element_f([output, prompt])
|
new_elements_f([output, prompt])
|
||||||
terminal_window.removeEventListener("keydown", terminal_event)
|
terminal_window.removeEventListener("keydown", terminal_event)
|
||||||
}
|
}
|
||||||
} else if (keyboard_event.key === Key.ArrowUp) {
|
} else if (keyboard_event.key === Key.ArrowUp) {
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
let working_dir = "user"
|
|
||||||
|
|
||||||
const enum EntryType {
|
|
||||||
Directory,
|
|
||||||
File
|
|
||||||
}
|
|
||||||
const enum Permissions {
|
|
||||||
r,
|
|
||||||
w,
|
|
||||||
rw
|
|
||||||
}
|
|
||||||
|
|
||||||
type File = string
|
|
||||||
type Entry<T> = {
|
|
||||||
readonly inner: T,
|
|
||||||
readonly type: EntryType,
|
|
||||||
readonly permissions: Permissions
|
|
||||||
}
|
|
||||||
function Entry<T = File>(inner: T, permissions: Permissions): Entry<T> {
|
|
||||||
const type = typeof inner == "object" ? EntryType.Directory : EntryType.File
|
|
||||||
return {
|
|
||||||
inner: inner,
|
|
||||||
type: type,
|
|
||||||
permissions: permissions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
["about_me.txt"]: Entry("", Permissions.rw),
|
|
||||||
["services.txt"]: Entry("", Permissions.rw)
|
|
||||||
}
|
|
||||||
const home = {
|
|
||||||
["user"]: Entry(user, Permissions.rw)
|
|
||||||
}
|
|
||||||
const root = {
|
|
||||||
["bin"]: Entry({}, Permissions.r),
|
|
||||||
["home"]: Entry(home, Permissions.r)
|
|
||||||
}
|
|
||||||
const fs = {
|
|
||||||
["/"]: Entry(root, Permissions.r)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
fs,
|
|
||||||
working_dir
|
|
||||||
}
|
|
75
src/components/react/shell/fs/fn.ts
Normal file
75
src/components/react/shell/fs/fn.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { Entry, EntryType, fs, type FsEntrySignature } from "./fs"
|
||||||
|
|
||||||
|
let working_dir = ["/", "home", "user"]
|
||||||
|
|
||||||
|
function get_working_dir_name(): string {
|
||||||
|
return working_dir[working_dir.length-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_working_dir_name_full(): string {
|
||||||
|
const w_dir_clone = [...working_dir]
|
||||||
|
const root = w_dir_clone.shift()
|
||||||
|
if (root) {
|
||||||
|
return root+w_dir_clone.join("/")
|
||||||
|
}
|
||||||
|
return "shift-error"
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum SetDirStatus {
|
||||||
|
Valid,
|
||||||
|
NotFound,
|
||||||
|
NotADirectory
|
||||||
|
}
|
||||||
|
interface FsIterEntry {
|
||||||
|
readonly entry: FsEntrySignature | null,
|
||||||
|
readonly status: SetDirStatus
|
||||||
|
}
|
||||||
|
function iter_fs_to_goal(w_dir_clone: string[]): FsIterEntry {
|
||||||
|
let next_iter = fs[0]
|
||||||
|
|
||||||
|
for (const w_dir of w_dir_clone) {
|
||||||
|
if (w_dir === "/") { continue }
|
||||||
|
if (next_iter.inner) {
|
||||||
|
const found = next_iter.inner.find(entry => entry.name === w_dir)
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return { entry: null, status: SetDirStatus.NotFound }
|
||||||
|
}
|
||||||
|
if (found.type !== EntryType.Directory) {
|
||||||
|
return { entry: null, status: SetDirStatus.NotADirectory }
|
||||||
|
}
|
||||||
|
if (found.name === w_dir_clone[w_dir_clone.length-1]) {
|
||||||
|
return { entry: next_iter, status: SetDirStatus.Valid }
|
||||||
|
} else {
|
||||||
|
next_iter = found.inner as FsEntrySignature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { entry: null, status: SetDirStatus.NotFound }
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_working_dir(name: string): SetDirStatus {
|
||||||
|
if (name === ".") { return SetDirStatus.Valid }
|
||||||
|
|
||||||
|
const w_dir_clone = [...working_dir]
|
||||||
|
if (name === "..") { w_dir_clone.pop() } else { w_dir_clone.push(name) }
|
||||||
|
|
||||||
|
const iter_status = iter_fs_to_goal(w_dir_clone)
|
||||||
|
if (iter_status.status === SetDirStatus.Valid) {
|
||||||
|
working_dir = w_dir_clone
|
||||||
|
}
|
||||||
|
return iter_status.status
|
||||||
|
}
|
||||||
|
|
||||||
|
function working_dir_entries() {
|
||||||
|
const w_dir_clone = [...working_dir]
|
||||||
|
const iter_status = iter_fs_to_goal(w_dir_clone)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
get_working_dir_name,
|
||||||
|
get_working_dir_name_full,
|
||||||
|
set_working_dir,
|
||||||
|
SetDirStatus
|
||||||
|
}
|
51
src/components/react/shell/fs/fs.ts
Normal file
51
src/components/react/shell/fs/fs.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const enum EntryType {
|
||||||
|
Directory,
|
||||||
|
File
|
||||||
|
}
|
||||||
|
const enum Permissions {
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
rw
|
||||||
|
}
|
||||||
|
|
||||||
|
type FsEntrySignature = Entry<Entry<{}>[]> //I did this!
|
||||||
|
|
||||||
|
const user = [
|
||||||
|
Entry("about_me.txt", "about me inside", Permissions.rw),
|
||||||
|
Entry("services.txt", "services inside", Permissions.rw),
|
||||||
|
Entry("hi", [], Permissions.rw)
|
||||||
|
]
|
||||||
|
const home = [
|
||||||
|
Entry("user", user, Permissions.rw)
|
||||||
|
]
|
||||||
|
const root = [
|
||||||
|
Entry("home", home, Permissions.r),
|
||||||
|
Entry("bin", {}, Permissions.r),
|
||||||
|
]
|
||||||
|
const fs = [
|
||||||
|
Entry("/", root, Permissions.r)
|
||||||
|
]
|
||||||
|
|
||||||
|
type File = string
|
||||||
|
interface Entry<T = File> {
|
||||||
|
readonly inner?: T,
|
||||||
|
readonly name: string,
|
||||||
|
readonly type: EntryType,
|
||||||
|
readonly permissions: Permissions
|
||||||
|
}
|
||||||
|
function Entry<T = File>(name: string, inner: T, permissions: Permissions): Entry<T> {
|
||||||
|
return {
|
||||||
|
type: typeof inner == "object" ? EntryType.Directory : EntryType.File,
|
||||||
|
inner: inner,
|
||||||
|
name: name,
|
||||||
|
permissions: permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
fs,
|
||||||
|
type FsEntrySignature,
|
||||||
|
EntryType,
|
||||||
|
Permissions,
|
||||||
|
Entry
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { working_dir } from "./fs"
|
import { get_working_dir_name } from "./fs/fn"
|
||||||
import { cyan, green } from "./color"
|
import { cyan, green } from "./color"
|
||||||
|
|
||||||
const userAgent = navigator.userAgent
|
const userAgent = navigator.userAgent
|
||||||
@ -9,13 +9,14 @@ if (browser_name_fallible) {
|
|||||||
browser_name = browser_name_fallible[0] === "Firefox" ? "gecko" : "chromium"
|
browser_name = browser_name_fallible[0] === "Firefox" ? "gecko" : "chromium"
|
||||||
}
|
}
|
||||||
|
|
||||||
function GetWorkingDir() {
|
function working_dir() {
|
||||||
return working_dir === "user" ? "~" : working_dir
|
const name = get_working_dir_name()
|
||||||
|
return name === "user" ? "~" : name
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Display() {
|
export default function Display() {
|
||||||
const user = cyan("user")
|
const user = cyan("user")
|
||||||
const dir = green(GetWorkingDir())
|
const dir = green(working_dir())
|
||||||
return <p>{user}@{browser_name} {dir}{"> "}</p>
|
return <p>{user}@{browser_name} {dir}{"> "}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,9 +20,9 @@ type newElement = (elements: JSX.Element[]) => void
|
|||||||
export default function Shell() {
|
export default function Shell() {
|
||||||
if (terminal_window) {
|
if (terminal_window) {
|
||||||
const [renderedElements, renderElement] = useState([display_prompt()])
|
const [renderedElements, renderElement] = useState([display_prompt()])
|
||||||
const new_element_f = (elements: JSX.Element[]) => renderElement([...renderedElements, ...elements])
|
const new_elements_f = (elements: JSX.Element[]) => renderElement([...renderedElements, ...elements])
|
||||||
|
|
||||||
keyboard_events(terminal_window, new_element_f)
|
keyboard_events(terminal_window, new_elements_f)
|
||||||
|
|
||||||
return renderedElements.map((element, k) => <React.Fragment key={k}>{element}</React.Fragment>)
|
return renderedElements.map((element, k) => <React.Fragment key={k}>{element}</React.Fragment>)
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ import Footer from "../components/footer.astro"
|
|||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
--body-background-color: rgb(0,0,0);
|
--body-background-color: rgb(0,0,0);
|
||||||
--hf-button-hover-color: rgb(50,50,50);
|
--hf-button-hover-color: rgb(255,255,255);
|
||||||
}
|
}
|
||||||
::selection {
|
::selection {
|
||||||
background-color: rgb(255,255,255);
|
background-color: rgb(255,255,255);
|
||||||
|
Reference in New Issue
Block a user