24 Commits

Author SHA1 Message Date
62ba198b40 fix indexing history upwards to be more accurate 2025-02-22 21:37:05 -05:00
93217ac064 header is now astro-less 2025-02-22 21:11:54 -05:00
9feda01532 clear git cache for new .gitignore
t
2025-02-22 17:11:55 -05:00
bbb4c60328 change the dist directory for docker 2025-02-22 17:08:40 -05:00
c2ab7e2499 vite.config.js 2025-02-22 16:35:38 -05:00
0a3077c016 move the public directory into src 2025-02-22 16:35:30 -05:00
ac88ebc83a remove more astro elements 2025-02-22 15:40:43 -05:00
dc082b0ddf move components/client to rt (runtime) directory 2025-02-22 15:40:33 -05:00
8dee9cdeff scss styling and remove astro elements 2025-02-22 15:37:39 -05:00
b8a34add56 index.html 2025-02-22 15:37:14 -05:00
ebe33a126b begin an scss file structure concept 2025-02-22 15:30:22 -05:00
5620b65511 start the process of moving over to vite tooling 2025-02-22 15:29:53 -05:00
bf40d524b7 fix history indexing down 2025-02-22 01:39:21 -05:00
1fe21b1592 move builtin commands out of list.ts into their own files 2025-02-22 01:16:37 -05:00
c5692b1b7f there is now a subcommand system and history command 2025-02-22 00:49:47 -05:00
b5f279691c update tsconfig.json to be more strict 2025-02-22 00:23:55 -05:00
4663aca074 create the builtin directory and move history.ts to it 2025-02-21 23:49:03 -05:00
e468155bb1 ::spelling-error for chromium browsers 2025-02-21 20:18:07 -05:00
1e938b19b0 rename run.ts -> command.ts 2025-02-21 17:34:59 -05:00
4f5602a5df builder for commands now and history help 2025-02-20 22:01:04 -05:00
129d0ff6b4 the prompt and command input works but the history does not 2025-02-18 17:36:54 -05:00
578aebcae1 init the docker file for later user
https://github.com/static-web-server/static-web-server
2025-02-18 17:31:56 -05:00
375427b16e the ps1 prompt displays properly 2025-02-17 15:02:06 -05:00
84e7089140 no more react 2025-02-16 19:36:08 -05:00
51 changed files with 2234 additions and 1583 deletions

View File

@ -1,4 +0,0 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored
View File

@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@ -0,0 +1,7 @@
FROM oven/bun AS build
COPY package.json astro.config.mjs /tmp/
COPY src public /tmp/
RUN bun run build

View File

@ -1,15 +0,0 @@
// @ts-check
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
integrations: [react({
babel: {
plugins: [
["babel-plugin-react-compiler"]
]
}
})]
});

1065
bun.lock

File diff suppressed because it is too large Load Diff

1458
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,17 @@
{ {
"name": "rhpidfyre.io", "name": "rhpidfyre.io",
"type": "module", "private": true,
"version": "0.0.1", "version": "0.0.1",
"scripts": { "type": "module",
"dev": "astro dev", "scripts": {
"build": "astro build", "dev": "vite",
"preview": "astro preview", "build": "tsc && vite build",
"astro": "astro" "preview": "vite preview"
}, },
"dependencies": { "devDependencies": {
"@astrojs/react": "^4.2.0", "sass": "^1.85.0",
"@types/react": "^19.0.8", "typescript": "^5.7.3",
"@types/react-dom": "^19.0.3", "vite": "^6.1.1",
"astro": "^5.3.0", "vite-plugin-html": "^3.2.2"
"react": "^19.0.0", }
"react-dom": "^19.0.0",
"sass": "^1.84.0",
"scss": "^0.2.4"
},
"devDependencies": {
"babel-plugin-react-compiler": "^19.0.0-beta-30d8a17-20250209",
"eslint-plugin-react-compiler": "^19.0.0-beta-30d8a17-20250209"
}
} }

View File

@ -1,25 +0,0 @@
---
interface Props {
href: string,
display: string,
color?: string
}
const {href, display, color = "transparent"} = Astro.props
---
<button style={`background-color: ${color}`}>
<a href={href}>{display}</a>
</button>
<style lang="scss">
button {
height: 100%;
padding: 0 20px 0 20px;
&:hover {
background-color: var(--hf-button-hover-color) !important;
& > a { color: black }
}
}
</style>

View File

@ -1,46 +0,0 @@
---
---
<footer>
<p class="raw-text" id="time">00:00:00</p>
<section>
<p class="raw-text" id="column-line">0, 0</p>
<button id="toggle-monospace">Monospace</button>
<button id="toggle-term-mode">UNIX</button>
</section>
<noscript>
<style>
#toggle-monospace, .raw-text { display: none }
</style>
</noscript>
</footer>
<style lang="scss">
@use "../scss/variables";
footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100vw;
height: variables.$footer-Y;
background-color: var(--body-background-color)
}
p {
padding: 0 20px 0 20px;
}
section {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
button {
height: 100%;
padding: 0 20px 0 20px;
&:hover { background-color: var(--hf-button-hover-color) }
}
}
#toggle-term-mode { background-color: rgb(0,50,90) }
</style>

View File

@ -1,28 +0,0 @@
---
import Button from './button.astro'
import { Links } from '../ts/links';
---
<header>
<Button href={Links.Self} display="[rhpidfyre.io]" color="black"/>
<section>
<Button href={Links.Git} display="GIT"/>
<Button href={Links.Cloud} display="CLOUD"/>
<Button href={Links.Gsm} display="GSM"/>
</section>
</header>
<style lang="scss">
@use "../scss/variables";
header {
display: flex;
justify-content: space-between;
width: 100vw;
height: variables.$header-Y;
background-color: rgb(10,10,10);
p { margin-left: 10px; }
}
section { height: 100%; }
</style>

View File

@ -1,3 +0,0 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width"/>
<meta name="generator" content={Astro.generator}/>

View File

@ -1,17 +0,0 @@
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
}

View File

@ -1,53 +0,0 @@
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

View File

@ -1,37 +0,0 @@
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 : <></>
}

View File

@ -1,60 +0,0 @@
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) {
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) {
} else if (keyboard_event.key === Key.ArrowDown) {
} else if (keyboard_event.key === Key.Tab) {
}
}
terminal_window.addEventListener("keydown", terminal_event)
}
export {
keyboard_events,
display_prompt
}

View File

@ -1,23 +0,0 @@
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 }

View File

@ -1,35 +0,0 @@
import { useState, type JSX } from "react"
import { red } from "../shell/color"
import { display_prompt, keyboard_events } from "../shell/events"
import React from "react"
const terminal_window = document.querySelector("main")
function panic(message: string) {
return <>
<p>{red("=================================================")}</p>
<p>{red("An unexpected JavaScript error occured:")}</p>
<p>{red(message)}</p>
<p>{red("=================================================")}</p>
</>
}
type newElement = (elements: JSX.Element[]) => void
export default function Shell() {
if (terminal_window) {
const [renderedElements, renderElement] = useState([display_prompt()])
const new_elements_f = (elements: JSX.Element[]) => renderElement([...renderedElements, ...elements])
keyboard_events(terminal_window, new_elements_f)
return renderedElements.map((element, k) => <React.Fragment key={k}>{element}</React.Fragment>)
}
return panic("The <main> element is missing")
}
export {
panic,
type newElement,
}

View File

@ -1,21 +0,0 @@
---
import { Links } from "../../ts/links"
---
<p>Welcome to rhpidfyre.io!</p>
<div class="return"></div>
<p>This is a personal website by rhpidfyre / Brandon.</p>
<p>You can find my services here or learn about me.</p>
<div class="return"></div>
<p>You can also contribute or view the source code of my website via the links:</p>
<p>{"<"}<a href={Links.RepoGitea} target="_blank">{Links.RepoGitea}</a>{">."}</p>
<p>{"<"}<a href={Links.RepoGithub} target="_blank">{Links.RepoGithub}</a>{">."}</p>
<div class="return"></div>
<p>You can get started with the command: <span class="bold">help</span></p>
<div class="return"></div>
<noscript>
<p><span class="red">=================================================</span></p>
<p><span class="red">JavaScript is disabled, functionality will be limited. :(</span></p>
<p><span class="red">But, you will not be limited at exploring my services which you can find by navigating towards the top-right.</span></p>
<p><span class="red">=================================================</span></p>
</noscript>

61
src/index.html Normal file
View File

@ -0,0 +1,61 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width"/>
<link rel="icon" type="image/png" href="/logo.png"/>
<link rel="stylesheet" href="./scss/global.scss"/>
<title>rhpidfyre.io</title>
</head>
<body>
<header>
<button class="header-left">
<a href="https://rhpidfyre.io/" class="header-button">[rhpidfyre.io]</a>
</button>
<section class="header-right">
<button>
<a href="https://git.rhpidfyre.io/" class="header-button">GIT</a>
</button>
<button>
<a href="https://files.rhpidfyre.io/" class="header-button">CLOUD</a>
</button>
<button>
<a href="https://gsm.rhpidfyre.io/" class="header-button">GSM</a>
</button>
</section>
</header>
<main>
<p>Welcome to rhpidfyre.io!</p>
<div class="return"></div>
<p>This is a personal website by rhpidfyre / Brandon.</p>
<p>You can find my services here or learn about me.</p>
<div class="return"></div>
<p>You can also contribute or view the source code of my website via the links:</p>
<p>&lt;<a href="https://git.rhpidfyre.io/rhpidfyre/rhpidfyre.io" target="_blank">https://git.rhpidfyre.io/rhpidfyre/rhpidfyre.io</a>&gt;.</p>
<p>&lt;<a href="https://github.com/unixtensor/rhpidfyre.io" target="_blank">https://github.com/unixtensor/rhpidfyre.io</a>&gt;.</p>
<div class="return"></div>
<p>You can get started with the command: <span class="bold">help</span></p>
<div class="return"></div>
<noscript>
<p><span class="red">=================================================</span></p>
<p><span class="red">JavaScript is disabled, functionality will be limited. :(</span></p>
<p><span class="red">But, you will not be limited at exploring my services which you can find by navigating towards the top-right.</span></p>
<p><span class="red">=================================================</span></p>
</noscript>
</main>
<footer>
<p class="raw-text" id="time">00:00:00</p>
<section>
<p class="raw-text" id="column-line">0, 0</p>
<button id="toggle-monospace">Monospace</button>
<button id="toggle-term-mode">UNIX</button>
</section>
<noscript>
<style>
#toggle-monospace, .raw-text { display: none }
</style>
</noscript>
</footer>
<script type="module" src="./rt/terminal.ts"></script>
</body>
</html>

View File

@ -1,50 +0,0 @@
---
import Metas from "../components/metas.astro"
import Header from "../components/header.astro"
import Footer from "../components/footer.astro"
---
<!doctype html>
<html lang="en">
<head>
<Metas/>
<link rel="icon" type="image/png" href="/logo.png"/>
<title>rhpidfyre.io</title>
</head>
<body>
<Header/>
<slot/>
<Footer/>
</body>
</html>
<style is:global lang="scss">
@forward "../scss/fonts";
:root {
color-scheme: dark;
--body-background-color: rgb(0,0,0);
--hf-button-hover-color: rgb(255,255,255);
}
::selection {
background-color: rgb(255,255,255);
color: rgb(0,0,0);
}
body {
box-sizing: border-box;
margin: 0;
overflow: hidden;
background-color: var(--body-background-color);
}
button {
background-color: transparent;
border: 0;
}
a {
color: white;
cursor: unset;
&:hover, &:active, &:link { text-decoration: none; }
}
</style>

View File

@ -1,37 +0,0 @@
---
import Webpage from '../layouts/Webpage.astro';
import Motd from '../components/terminal/motd.astro';
import Terminal from '../components/react/terminal/exec';
---
<Webpage>
<main>
<Motd/>
<Terminal client:only="react"/>
</main>
</Webpage>
<style lang="scss" is:global>
@use "../scss/variables";
@use "../scss/terminal";
main {
@include terminal.formatting;
width: 100vw;
height: calc(99.3vh - variables.$header-Y - variables.$footer-Y);
padding: 5px;
overflow-y: auto;
}
input {
font-size: 1.2rem;
background-color: transparent;
border: 0;
outline: 0;
width: 90%;
/* Pester me when this gets undrafted */
caret-shape: block;
&:disabled { color: white }
}
</style>

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

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
}

View File

@ -2,7 +2,7 @@ import { Entry, EntryType, fs, type FsEntrySignature } from "./fs"
let working_dir = ["/", "home", "user"] let working_dir = ["/", "home", "user"]
function get_working_dir_name(): string { function get_working_dir_name() {
return working_dir[working_dir.length-1] return working_dir[working_dir.length-1]
} }
@ -29,7 +29,7 @@ function iter_fs_to_goal(w_dir_clone: string[]): FsIterEntry {
for (const w_dir of w_dir_clone) { for (const w_dir of w_dir_clone) {
if (w_dir === "/") { continue } if (w_dir === "/") { continue }
if (next_iter.inner) { if (next_iter && next_iter.inner) {
const found = next_iter.inner.find(entry => entry.name === w_dir) const found = next_iter.inner.find(entry => entry.name === w_dir)
if (!found) { if (!found) {

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[0] !== cmd) {
this.file.inner.unshift(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 {
}

View File

@ -0,0 +1,27 @@
@use "../variables.scss";
@mixin styling {
display: flex;
justify-content: space-between;
align-items: center;
width: 100vw;
height: variables.$footer-Y;
background-color: var(--body-background-color)
p { padding: 0 20px 0 20px; }
section {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
button {
height: 100%;
padding: 0 20px 0 20px;
&:hover { background-color: var(--hf-button-hover-color) }
}
}
#toggle-term-mode { background-color: rgb(0,50,90) }
}

View File

@ -0,0 +1,32 @@
@use "../variables.scss";
@mixin navigation {
.header-left {
height: 100%;
padding: 0 20px 0 20px;
background-color: rgb(0,0,0);
}
.header-right {
display: flex;
height: 100%;
button {
padding: 0 20px 0 20px;
&:hover {
background-color: white;
& > a { color: black; }
}
}
}
}
@mixin styling {
@include navigation;
display: flex;
justify-content: space-between;
align-items: center;
width: 100vw;
height: variables.$header-Y;
background-color: rgb(10,10,10);
}

View File

@ -0,0 +1,25 @@
@use "./terminal";
@use "../variables.scss";
@mixin input {
font-size: 1.2rem;
background-color: transparent;
border: 0;
outline: 0;
width: 90%;
/* Pester me when this gets undrafted */
caret-shape: block;
&:disabled { color: white }
}
@mixin styling {
@include terminal.formatting;
width: 100vw;
height: calc(99.3vh - variables.$header-Y - variables.$footer-Y);
padding: 5px;
overflow-y: auto;
input { @include input; }
}

View File

@ -9,9 +9,22 @@
.bold { font-weight: bold; } .bold { font-weight: bold; }
} }
@mixin formatting { @mixin stdout-layouts {
.stdout-vertical { display: grid; }
.stdout-horizontal {
display: flex;
gap: 30px;
}
}
@mixin term-elements {
.return { margin-top: 25px; } .return { margin-top: 25px; }
.shell-prompt { display: flex; } .shell-prompt { display: flex; }
}
@mixin formatting {
@include stdout-layouts;
@include term-elements;
p { p {
@include color-matrix; @include color-matrix;

View File

@ -1,8 +1,10 @@
@font-face { @font-face {
font-display: swap; font: {
font-family: 'Terminus'; family: 'Terminus';
font-style: normal; display: swap;
font-weight: 500; style: normal;
weight: 500;
};
src: url('/Terminus.woff2') format('woff2'); src: url('/Terminus.woff2') format('woff2');
} }

41
src/scss/global.scss Normal file
View File

@ -0,0 +1,41 @@
@forward "./font.scss";
@use "./variables.scss";
@use "./elements/header.scss";
@use "./elements/main.scss";
@use "./elements/footer.scss";
:root {
color-scheme: dark;
--body-background-color: rgb(0,0,0);
--hf-button-hover-color: rgb(255,255,255);
}
::selection {
background-color: rgb(255,255,255);
color: rgb(0,0,0);
}
::spelling-error {
text-decoration: none
}
body {
box-sizing: border-box;
margin: 0;
overflow: hidden;
background-color: var(--body-background-color);
}
button {
background-color: transparent;
border: 0;
}
a {
color: white;
cursor: unset;
&:hover, &:active, &:link { text-decoration: none; }
}
header { @include header.styling; }
main { @include main.styling; }
footer { @include footer.styling; }

View File

@ -1,10 +0,0 @@
const enum Links {
Self = "https://rhpidfyre.io/",
Cloud = "https://files.rhpidfyre.io/",
Gsm = "https://gsm.rhpidfyre.io/",
Git = "https://git.rhpidfyre.io/",
RepoGitea = "https://git.rhpidfyre.io/rhpidfyre/rhpidfyre.io/",
RepoGithub = "https://github.com/unixtensor/rhpidfyre.io/"
}
export { Links }

View File

@ -1,15 +1,24 @@
{ {
"extends": "astro/tsconfigs/strict",
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
],
"compilerOptions": { "compilerOptions": {
"noImplicitAny": true, "target": "ES2020",
"jsx": "react-jsx", "useDefineForClassFields": true,
"jsxImportSource": "react" "module": "ESNext",
} "lib": ["ES2020", "DOM", "DOM.Iterable"],
} "skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
// "noUnusedLocals": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"noImplicitAny": true,
},
"include": ["src"]
}

12
vite.config.js Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'
export default defineConfig({
plugins: [
createHtmlPlugin({minify: true}),
],
root: "src",
build: {
outDir: "../dist"
}
})