6 Commits

11 changed files with 246 additions and 73 deletions

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
}

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

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

@ -2,8 +2,8 @@ import { bold } from "../color";
import { to_args, trim } from "./parse"; import { to_args, trim } from "./parse";
import commands, { type Command } from "./list"; import commands, { type Command } from "./list";
import create from "../../elements/create";
import history from "../history"; import history from "../history";
import stdout from "../../elements/stdout";
type Term = HTMLElement type Term = HTMLElement
@ -18,8 +18,7 @@ function valid_command(term: Term, args: string[]) {
} }
function unknown_command(cmd_name: string) { function unknown_command(cmd_name: string) {
const unknown_element = create("p") const unknown_element = stdout("shell: Unknown command: ")
unknown_element.innerText = "shell: Unknown command: "
unknown_element.appendChild(bold(cmd_name)) unknown_element.appendChild(bold(cmd_name))
return unknown_element return unknown_element
} }
@ -34,4 +33,6 @@ export default function run(term: Term, stdin: string) {
} }
history.add(args.join(" ")) history.add(args.join(" "))
return false return false
} }
export { unknown_command }

View File

@ -1,17 +1,11 @@
import { bold } from "../color"
import { get_working_dir_name_full, set_working_dir, SetDirStatus } from "../fs/fn" import { get_working_dir_name_full, set_working_dir, SetDirStatus } from "../fs/fn"
import create from "../../elements/create" import history_cmd from "./builtin/history"
import stdout from "../../elements/stdout"
type Term = HTMLElement type Term = HTMLElement
type Args = string[] type Args = string[]
function strout(term: Term, s: string) {
const p = create("p")
p.innerText = s
term.appendChild(p)
}
function clear(term: Term, args: Args): boolean { function clear(term: Term, args: Args): boolean {
Array.from(term.children).forEach(node => { Array.from(term.children).forEach(node => {
if (node.tagName === "DIV") { if (node.tagName === "DIV") {
@ -52,7 +46,7 @@ function ls(term: Term, args: Args): boolean {
} }
function pwd(term: Term, args: Args): boolean { function pwd(term: Term, args: Args): boolean {
strout(term, get_working_dir_name_full()) term.appendChild(stdout(get_working_dir_name_full()))
return true return true
} }
@ -70,7 +64,12 @@ const commands: CommandsList = {
["ls"]: ls, ["ls"]: ls,
["pwd"]: pwd, ["pwd"]: pwd,
["cat"]: cat, ["cat"]: cat,
["history"]: history_cmd,
} }
export default commands export default commands
export { type Command } export {
type Command,
type Term,
type 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

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

View File

@ -1,9 +1,6 @@
import { history_list } from "./shell/history" import history from "./shell/history"
import prompt from "./elements/prompt" import prompt from "./elements/prompt"
import run from "./shell/command/run" import keys from "./keys"
let history_index = 0
const term_win_unsafe = document.querySelector("main") const term_win_unsafe = document.querySelector("main")
const enum Key { const enum Key {
@ -14,47 +11,11 @@ const enum Key {
Tab = "Tab" Tab = "Tab"
} }
type InputClosure = (key_event: KeyboardEvent) => void
interface EnterArgs {
readonly term_win_safe: HTMLElement,
readonly ps1input: HTMLInputElement,
readonly closure: InputClosure
}
function key_enter(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)
spawnps1(input.term_win_safe)
}
function key_up_arrow(ps1input: HTMLInputElement) {
const history_item = history_list[history_index]
if (history_item) {
ps1input.value = history_item
history_index+=1
}
}
function key_down_arrow(ps1input: HTMLInputElement) {
const history_item = history_list[history_index]
if (history_item) {
history_index-=1
if (history_index === -1) {
history_index = 0
} else {
ps1input.value = history_item
}
}
}
function spawnps1(term_win_safe: HTMLElement) { function spawnps1(term_win_safe: HTMLElement) {
const ps1prompt = prompt() const ps1prompt = prompt()
term_win_safe.appendChild(ps1prompt.body) term_win_safe.appendChild(ps1prompt.body)
bind_processor(term_win_safe, ps1prompt.input) bind_processor(term_win_safe, ps1prompt.input)
history_index = 0 history.file.cursor_reset()
ps1prompt.input.focus() ps1prompt.input.focus()
} }
@ -62,11 +23,12 @@ function bind_processor(term_win_safe: HTMLElement, ps1prompt_input: HTMLInputEl
const input_closure = (key_event: KeyboardEvent) => { const input_closure = (key_event: KeyboardEvent) => {
if (key_event.key === Key.Enter) { if (key_event.key === Key.Enter) {
key_event.preventDefault() key_event.preventDefault()
key_enter({ keys.enter({
term_win_safe: term_win_safe, term_win_safe: term_win_safe,
ps1input: ps1prompt_input, ps1input: ps1prompt_input,
closure: input_closure closure: input_closure
}) })
spawnps1(term_win_safe)
} else if (key_event.key === Key.Tab) { } else if (key_event.key === Key.Tab) {
key_event.preventDefault() key_event.preventDefault()
@ -75,10 +37,10 @@ function bind_processor(term_win_safe: HTMLElement, ps1prompt_input: HTMLInputEl
} else if (key_event.key === Key.ArrowUp) { } else if (key_event.key === Key.ArrowUp) {
key_event.preventDefault() key_event.preventDefault()
key_up_arrow(ps1prompt_input) keys.up_arrow(ps1prompt_input)
} else if (key_event.key === Key.ArrowDown) { } else if (key_event.key === Key.ArrowDown) {
key_event.preventDefault() key_event.preventDefault()
key_down_arrow(ps1prompt_input) keys.down_arrow(ps1prompt_input)
} }
} }
ps1prompt_input.addEventListener("keydown", input_closure) ps1prompt_input.addEventListener("keydown", input_closure)

View File

@ -14,6 +14,10 @@ import Motd from '../components/terminal/motd.astro';
@use "../scss/variables"; @use "../scss/variables";
@use "../scss/terminal"; @use "../scss/terminal";
::spelling-error {
text-decoration: none
}
main { main {
@include terminal.formatting; @include terminal.formatting;

View File

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

View File

@ -1,8 +1,10 @@
{ {
"extends": "astro/tsconfigs/strict", "extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"], "include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"], "exclude": ["dist"],
"compilerOptions": { "compilerOptions": {
"noImplicitAny": true "noImplicitAny": true,
} "noUnusedLocals": true,
"allowUnusedLabels": false
}
} }