no more react
This commit is contained in:
19
src/components/client/shell/color.ts
Normal file
19
src/components/client/shell/color.ts
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
const red = (s: string) => <span className="red">{s}</span>
|
||||
const green = (s: string) => <span className="green">{s}</span>
|
||||
const blue = (s: string) => <span className="blue">{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) {
|
||||
return <span style={{color: `rgb(${Ru8},${Gu8},${Bu8})`}}>{s}</span>
|
||||
}
|
||||
|
||||
export {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
cyan,
|
||||
bold
|
||||
}
|
53
src/components/client/shell/command/list.ts
Normal file
53
src/components/client/shell/command/list.ts
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
|
37
src/components/client/shell/command/run.ts
Normal file
37
src/components/client/shell/command/run.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { JSX } from "react";
|
||||
import commands from "./list";
|
||||
import { bold } from "../color";
|
||||
|
||||
function trim(stdin: string): string {
|
||||
const trimmed_str: string[] = []
|
||||
stdin.split(" ").forEach(s => { if (s !== "") { trimmed_str.push(s) } })
|
||||
return trimmed_str.join(" ")
|
||||
}
|
||||
|
||||
function to_args(trimmed_str: string): string[] {
|
||||
return trimmed_str.split(" ")
|
||||
}
|
||||
|
||||
function valid_command(args: string[]): JSX.Element | undefined {
|
||||
for (const command_in_list in commands) {
|
||||
const command = args[0]
|
||||
if (command === command_in_list) {
|
||||
return commands[command_in_list](args)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
function unknown_command(cmd_name: string) {
|
||||
return <p>{"shell: Unknown command: "}{bold(cmd_name)}</p>
|
||||
}
|
||||
|
||||
export default function run(stdin: string) {
|
||||
const args = to_args(trim(stdin))
|
||||
const command = valid_command(args)
|
||||
|
||||
if (args[0] !== "" && !command) {
|
||||
return unknown_command(args[0])
|
||||
}
|
||||
return command ? command : <></>
|
||||
}
|
62
src/components/client/shell/events.ts
Normal file
62
src/components/client/shell/events.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import Display from "./prompt"
|
||||
import run from "./command/run"
|
||||
|
||||
import { type newElement } from "../terminal/exec";
|
||||
import type { JSX } from "react";
|
||||
|
||||
const enum Key {
|
||||
Enter = "Enter",
|
||||
ArrowUp = "ArrowUp",
|
||||
ArrowDown = "ArrowDown",
|
||||
Tab = "Tab"
|
||||
}
|
||||
|
||||
function display_prompt() {
|
||||
return <div className="shell-prompt">
|
||||
<Display/>
|
||||
<input className="shell-ps1" type="text" spellCheck={false}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function get_current_prompt(): HTMLInputElement | undefined {
|
||||
const shell_input = document.getElementsByClassName("shell-ps1")
|
||||
|
||||
return shell_input[shell_input.length-1] as HTMLInputElement
|
||||
}
|
||||
|
||||
function new_prompt(): JSX.Element {
|
||||
const shell_prompts = document.getElementsByClassName("shell-ps1")
|
||||
|
||||
Array.from(shell_prompts).forEach(shellps1 => {
|
||||
(shellps1 as HTMLInputElement).disabled = true
|
||||
})
|
||||
return display_prompt()
|
||||
}
|
||||
|
||||
function keyboard_events(terminal_window: HTMLElement, new_elements_f: newElement) {
|
||||
const terminal_event = (keyboard_event: KeyboardEvent) => {
|
||||
if (keyboard_event.key === Key.Enter) {
|
||||
keyboard_event.preventDefault()
|
||||
const current_prompt = get_current_prompt()
|
||||
if (current_prompt) {
|
||||
const prompt = new_prompt()
|
||||
const output = run(current_prompt.value)
|
||||
new_elements_f([output, prompt])
|
||||
terminal_window.removeEventListener("keydown", terminal_event)
|
||||
}
|
||||
} else if (keyboard_event.key === Key.ArrowUp) {
|
||||
keyboard_event.preventDefault()
|
||||
} else if (keyboard_event.key === Key.ArrowDown) {
|
||||
keyboard_event.preventDefault()
|
||||
} else if (keyboard_event.key === Key.Tab) {
|
||||
keyboard_event.preventDefault()
|
||||
}
|
||||
}
|
||||
terminal_window.addEventListener("keydown", terminal_event)
|
||||
}
|
||||
|
||||
export {
|
||||
keyboard_events,
|
||||
display_prompt,
|
||||
get_current_prompt
|
||||
}
|
75
src/components/client/shell/fs/fn.ts
Normal file
75
src/components/client/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/client/shell/fs/fs.ts
Normal file
51
src/components/client/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
|
||||
}
|
23
src/components/client/shell/prompt.ts
Normal file
23
src/components/client/shell/prompt.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { get_working_dir_name } from "./fs/fn"
|
||||
import { cyan, green } from "./color"
|
||||
|
||||
const userAgent = navigator.userAgent
|
||||
const browser_name_fallible = userAgent.match(/Firefox.\d+[\d.\d]+|Chrome.\d+[\d.\d]+/gm)?.map(f => f.split("/")[0])
|
||||
|
||||
let browser_name = "unknown"
|
||||
if (browser_name_fallible) {
|
||||
browser_name = browser_name_fallible[0] === "Firefox" ? "gecko" : "chromium"
|
||||
}
|
||||
|
||||
function working_dir() {
|
||||
const name = get_working_dir_name()
|
||||
return name === "user" ? "~" : name
|
||||
}
|
||||
|
||||
export default function Display() {
|
||||
const user = cyan("user")
|
||||
const dir = green(working_dir())
|
||||
return <p>{user}@{browser_name} {dir}{"> "}</p>
|
||||
}
|
||||
|
||||
export { userAgent }
|
30
src/components/client/terminal/contextMenu.ts
Normal file
30
src/components/client/terminal/contextMenu.ts
Normal file
@ -0,0 +1,30 @@
|
||||
const terminal_window = document.querySelector("main")
|
||||
|
||||
interface PromptPosition {
|
||||
x: number,
|
||||
y: number,
|
||||
}
|
||||
|
||||
interface PromptContext extends PromptPosition {
|
||||
enabled: boolean,
|
||||
}
|
||||
|
||||
function Menu(pos: PromptPosition) {
|
||||
console.log(pos.x, pos.y)
|
||||
}
|
||||
|
||||
export default function ContextMenu() {
|
||||
if (terminal_window) {
|
||||
useEffect(() => {
|
||||
const [contextMenu, newContextMenu] = useState(false)
|
||||
const [_, toggleRightClick] = useState({enabled: false, x: 0, y: 0})
|
||||
|
||||
terminal_window.addEventListener("contextmenu", (menuEvent) => {
|
||||
menuEvent.preventDefault()
|
||||
toggleRightClick({enabled: true, x: menuEvent.pageX, y: menuEvent.pageY})
|
||||
Menu({x: menuEvent.pageX, y: menuEvent.pageY})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
}
|
34
src/components/client/terminal/exec.ts
Normal file
34
src/components/client/terminal/exec.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { red } from "../shell/color"
|
||||
import { display_prompt, keyboard_events } from "../shell/events"
|
||||
|
||||
const terminal_window = document.getElementById("interactive-area")
|
||||
|
||||
// function panic(message: string) {
|
||||
// return <>
|
||||
// <p>{red("=================================================")}</p>
|
||||
// <p>{red("An unexpected JavaScript error occured:")}</p>
|
||||
// <p>{red(message)}</p>
|
||||
// <p>{red("=================================================")}</p>
|
||||
// </>
|
||||
// }
|
||||
|
||||
// function ps1(terminal_window_safe: HTMLElement): JSX.Element[] {
|
||||
// const [renderedElements, renderElement] = useState([display_prompt()])
|
||||
// const new_elements_f = (elements: JSX.Element[]) => renderElement([...renderedElements, ...elements])
|
||||
|
||||
// keyboard_events(terminal_window_safe, new_elements_f)
|
||||
|
||||
// return renderedElements
|
||||
// }
|
||||
|
||||
// export default function Shell() {
|
||||
// if (terminal_window) {
|
||||
// return ps1(terminal_window).map((element, k) => <React.Fragment key={k}>{element}</React.Fragment>)
|
||||
// }
|
||||
// return panic("The <main> element is missing")
|
||||
// }
|
||||
|
||||
// export {
|
||||
// panic,
|
||||
// type newElement,
|
||||
// }
|
Reference in New Issue
Block a user