move components/client to rt (runtime) directory

This commit is contained in:
2025-02-22 15:40:33 -05:00
parent 8dee9cdeff
commit dc082b0ddf
25 changed files with 1 additions and 124 deletions

View File

@ -0,0 +1,9 @@
function create<T extends keyof HTMLElementTagNameMap>(element: T, className?: string): HTMLElementTagNameMap[T] {
const new_element = document.createElement(element)
if (className) {
new_element.className = className
}
return new_element
}
export default create

66
src/rt/elements/prompt.ts Normal file
View File

@ -0,0 +1,66 @@
import { cyan, green } from "../shell/color"
import { get_working_dir_name } from "../shell/fs/fn"
import create from "./create"
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"
}
interface Ps1Prompt {
readonly body: HTMLDivElement,
readonly input: HTMLInputElement
}
interface Inputs {
old?: HTMLInputElement,
new?: HTMLInputElement
}
let inputs: Inputs = {
old: undefined,
new: undefined
}
function ps1_element(user: HTMLSpanElement, dir: HTMLSpanElement) {
const display = create("p")
display.appendChild(user)
display.append(`@${browser_name}`)
display.appendChild(dir)
display.append("> ")
return display
}
function working_dir() {
const dir_name = get_working_dir_name()
return dir_name === "user" ? "~" : dir_name
}
function working_dir_element() {
const user = cyan("user")
const dir = green(" "+working_dir())
return ps1_element(user, dir)
}
export default function prompt(): Ps1Prompt {
const prompt_div = create("div", "shell-prompt")
const ps1 = working_dir_element()
const input = create("input", "shell-ps1")
input.type = "text"
input.spellcheck = false
inputs.old = inputs.new
if (inputs.old) {
inputs.old.disabled = true
}
inputs.new = input
prompt_div.appendChild(ps1)
prompt_div.appendChild(input)
return {
body: prompt_div,
input: input
}
}

43
src/rt/elements/stdout.ts Normal file
View File

@ -0,0 +1,43 @@
import { bold } from "../shell/color";
import create from "./create";
function stdout_grid(left: string[], right: string[]) {
const root = create("div", "stdout-horizontal")
const container_left = create("div", "stdout-vertical")
const container_right = create("div", "stdout-vertical")
left.forEach(str => container_left.appendChild(stdout_bold(str)))
right.forEach(str => container_right.appendChild(stdout(str)))
root.appendChild(container_left)
root.appendChild(container_right)
return root
}
function stdout_horizontal(strs: string[]) {
const p = create("p")
strs.forEach((str, i) => {
const tab = i !== strs.length-1 ? "\t" : ""
p.innerText+=str+tab
})
return p
}
function stdout_bold(str: string) {
const p = stdout("")
p.appendChild(bold(str))
return p
}
export default function stdout(str: string) {
const p = create("p")
p.innerText = str
return p
}
export {
stdout_grid,
stdout_horizontal,
stdout_bold
}

34
src/rt/keys.ts Normal file
View File

@ -0,0 +1,34 @@
import run from "./shell/command/command"
import history from "./shell/history"
type InputClosure = (key_event: KeyboardEvent) => void
interface EnterArgs {
readonly term_win_safe: HTMLElement,
readonly ps1input: HTMLInputElement,
readonly closure: InputClosure
}
interface Keys {
enter: (input: EnterArgs) => void,
up_arrow: (ps1input: HTMLInputElement) => void,
down_arrow: (ps1input: HTMLInputElement) => void,
}
const keys = {} as Keys
keys.enter = function(input: EnterArgs) {
const unknown_command_msg = run(input.term_win_safe, input.ps1input.value)
if (unknown_command_msg) {
input.term_win_safe.appendChild(unknown_command_msg)
}
input.ps1input.removeEventListener("keydown", input.closure)
}
keys.up_arrow = function(ps1input: HTMLInputElement) {
history.index_up(ps1input)
}
keys.down_arrow = function(ps1input: HTMLInputElement) {
history.index_down(ps1input)
}
export default keys

34
src/rt/shell/color.ts Normal file
View File

@ -0,0 +1,34 @@
import create from "../elements/create"
const enum Colors {
red = "red",
green = "green",
blue = "blue",
cyan = "cyan",
bold = "bold"
}
function newcolor(inner: string, color?: Colors) {
const span = create("span", color)
span.innerText = inner
return span
}
const red = (s: string) => newcolor(s, Colors.red )
const green = (s: string) => newcolor(s, Colors.green)
const blue = (s: string) => newcolor(s, Colors.blue)
const cyan = (s: string) => newcolor(s, Colors.cyan)
const bold = (s: string) => newcolor(s, Colors.bold)
export default function rgb(s: string, Ru8: number, Gu8: number, Bu8: number) {
const rgb_span = newcolor(s)
rgb_span.style.color = `rgb(${Ru8},${Gu8},${Bu8})`
return rgb_span
}
export {
red,
green,
blue,
cyan,
bold
}

View File

@ -0,0 +1,6 @@
import type { Args, Term } from "../list";
export default function cat(term: Term, args: Args): boolean {
return true
}

View File

@ -0,0 +1,13 @@
import { set_working_dir, SetDirStatus } from "../../fs/fn"
import type { Args, Term } from "../list"
export default function cd(term: Term, args: Args): boolean {
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
}

View File

@ -0,0 +1,19 @@
import type { Args, Term } from "../list"
export default function clear(term: Term, args: Args): boolean {
Array.from(term.children).forEach(node => {
if (node.tagName === "DIV") {
if (node.className === "shell-prompt") {
const input = node.getElementsByClassName("shell-ps1")[0] as HTMLInputElement
if (input.disabled || input.value === "clear") {
node.remove()
}
} else {
node.remove()
}
} else if (node.tagName === "P") {
node.remove()
}
})
return true
}

View File

@ -0,0 +1,22 @@
import type { Args, Term } from "../list";
import stdout from "../../../elements/stdout";
import SubCommand from "../subcommand";
import history from "../../history";
const history_command = new SubCommand("Show and manipulate command history")
history_command.add("show", "Show the history", function(term: Term, _args: Args) {
history.file.inner.forEach((entry, ind) => term.appendChild(stdout(`${ind} ${entry}`)))
})
history_command.add("clear", "Delete the entire command history", function(term: Term, _args: Args) {
const entries = history.file.inner.length
history.file.inner = []
term.appendChild(stdout(`Cleared ${entries} entries from the history.`))
})
export default function history_cmd(term: Term, args: Args) {
history_command.process(term, args)
return true
}

View File

@ -0,0 +1,11 @@
import type { Args, Term } from "../list";
export default function ls(term: Term, args: Args): boolean {
// if (args[1] === undefined) {
// for (const dir_name in working_dir) {
// }
// return <p>{`${working_dir}`}</p>
// }
return true
}

View File

@ -0,0 +1,9 @@
import { get_working_dir_name_full } from "../../fs/fn";
import type { Args, Term } from "../list";
import stdout from "../../../elements/stdout";
export default function pwd(term: Term, args: Args): boolean {
term.appendChild(stdout(get_working_dir_name_full()))
return true
}

View File

@ -0,0 +1,38 @@
import { bold } from "../color";
import { to_args, trim } from "./parse";
import commands, { type Command } from "./list";
import history from "../history";
import stdout from "../../elements/stdout";
type Term = HTMLElement
function valid_command(term: Term, args: string[]) {
for (const command_in_list in commands) {
const command = args[0]
if (command === command_in_list) {
return (commands[command_in_list] as Command)(term, args)
}
}
return
}
function unknown_command(cmd_name: string) {
const unknown_element = stdout("shell: Unknown command: ")
unknown_element.appendChild(bold(cmd_name))
return unknown_element
}
export default function run(term: Term, stdin: string) {
const args = to_args(trim(stdin))
const valid = valid_command(term, args)
const command = args[0] as string
if (command !== "" && !valid) {
return unknown_command(command)
}
history.add(args.join(" "))
return false
}
export { unknown_command }

View File

@ -0,0 +1,30 @@
import history from "./builtin/history"
import clear from "./builtin/clear"
import pwd from "./builtin/pwd"
import cat from "./builtin/cat"
import cd from "./builtin/cd"
import ls from "./builtin/ls"
type Term = HTMLElement
type Args = string[]
type Command = (term: Term, args: Args) => boolean
interface CommandsList {
[index: string]: Command,
}
const commands: CommandsList = {
["history"]: history,
["clear"]: clear,
["pwd"]: pwd,
["cat"]: cat,
["cd"]: cd,
["ls"]: ls,
}
export default commands
export {
type Command,
type Term,
type Args
}

View File

@ -0,0 +1,14 @@
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(" ")
}
export {
trim,
to_args
}

View File

@ -0,0 +1,63 @@
import stdout, { stdout_grid } from "../../elements/stdout";
import { bold } from "../color";
import type { Args, Term } from "./list";
type SubCommandClosure = (term: Term, args: Args) => void
interface SubCommandAction {
inner: SubCommandClosure,
description: string,
}
interface SubCommands {
[index: string]: SubCommandAction,
}
const SubCommand = class {
public data: SubCommands //data? less goo!
constructor(description: string) {
this.data = {}
this.data.help = {} as SubCommandAction
this.data.help.description = "Display help info"
this.data.help.inner = (term: Term, _args: Args) => {
const descriptions: string[] = []
Object.values(this.data).forEach(sub_cmd => descriptions.push(sub_cmd.description))
term.appendChild(stdout(description))
term.appendChild(stdout_grid(Object.keys(this.data), descriptions))
}
}
public process(term: Term, args: Args) {
const subc = args[1]
if (subc) {
const subc_f = this.data[subc]
if (subc_f) {
subc_f.inner(term, args)
} else {
term.appendChild(SubCommand.unknown(subc))
this.data.help.inner(term, args)
}
} else {
this.data.help.inner(term, args)
}
}
public add(name: string, description: string, f: SubCommandClosure) {
this.data[name] = {} as SubCommandAction
this.data[name].description = description
this.data[name].inner = f
}
public static unknown(subcmd_name: string) {
const subcmd_unknown = stdout("Unknown sub-command: ")
subcmd_unknown.appendChild(bold(subcmd_name))
return subcmd_unknown
}
}
export default SubCommand
export {
type SubCommand,
type SubCommandAction
}

75
src/rt/shell/fs/fn.ts Normal file
View File

@ -0,0 +1,75 @@
import { Entry, EntryType, fs, type FsEntrySignature } from "./fs"
let working_dir = ["/", "home", "user"]
function get_working_dir_name() {
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 && 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/rt/shell/fs/fs.ts Normal file
View 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
}

48
src/rt/shell/history.ts Normal file
View File

@ -0,0 +1,48 @@
interface HistoryFile {
inner: string[],
cursor: number,
cursor_reset: () => void
}
interface History {
file: HistoryFile
add: (cmd: string) => void,
index_up: (ps1input: HTMLInputElement) => void,
index_down: (ps1input: HTMLInputElement) => void
}
const history = {} as History
history.file = {} as HistoryFile
history.file.inner = []
history.file.cursor = 0
history.file.cursor_reset = function() {
this.cursor = 0
}
history.add = function(cmd: string) {
if (this.file.inner[this.file.inner.length-1] !== cmd) {
this.file.inner.push(cmd)
}
}
history.index_up = function(ps1input: HTMLInputElement) {
const item = this.file.inner[this.file.cursor]
if (item) {
this.file.cursor+=1
ps1input.value = item
}
}
history.index_down = function(ps1input: HTMLInputElement) {
if (this.file.cursor!==0) {
this.file.cursor-=1
if (this.file.cursor!==0) {
const item = this.file.inner[this.file.cursor-1]
if (item) { ps1input.value = item }
} else {
this.file.cursor_reset()
ps1input.value = ""
}
}
}
export default history

53
src/rt/terminal.ts Normal file
View File

@ -0,0 +1,53 @@
import history from "./shell/history"
import prompt from "./elements/prompt"
import keys from "./keys"
const term_win_unsafe = document.querySelector("main")
const enum Key {
Enter = "Enter",
ArrowRight = "ArrowRight",
ArrowUp = "ArrowUp",
ArrowDown = "ArrowDown",
Tab = "Tab"
}
function spawnps1(term_win_safe: HTMLElement) {
const ps1prompt = prompt()
term_win_safe.appendChild(ps1prompt.body)
bind_processor(term_win_safe, ps1prompt.input)
history.file.cursor_reset()
ps1prompt.input.focus()
}
function bind_processor(term_win_safe: HTMLElement, ps1prompt_input: HTMLInputElement) {
const input_closure = (key_event: KeyboardEvent) => {
if (key_event.key === Key.Enter) {
key_event.preventDefault()
keys.enter({
term_win_safe: term_win_safe,
ps1input: ps1prompt_input,
closure: input_closure
})
spawnps1(term_win_safe)
} else if (key_event.key === Key.Tab) {
key_event.preventDefault()
} else if (key_event.key === Key.ArrowRight) {
key_event.preventDefault()
} else if (key_event.key === Key.ArrowUp) {
key_event.preventDefault()
keys.up_arrow(ps1prompt_input)
} else if (key_event.key === Key.ArrowDown) {
key_event.preventDefault()
keys.down_arrow(ps1prompt_input)
}
}
ps1prompt_input.addEventListener("keydown", input_closure)
}
if (term_win_unsafe) {
spawnps1(term_win_unsafe)
} else {
}