Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
c5692b1b7f | |||
b5f279691c | |||
4663aca074 | |||
e468155bb1 | |||
1e938b19b0 | |||
4f5602a5df |
43
src/components/client/elements/stdout.ts
Normal file
43
src/components/client/elements/stdout.ts
Normal 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/components/client/keys.ts
Normal file
34
src/components/client/keys.ts
Normal 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
|
22
src/components/client/shell/command/builtin/history.ts
Normal file
22
src/components/client/shell/command/builtin/history.ts
Normal 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
|
||||||
|
}
|
@ -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 }
|
@ -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
|
||||||
|
}
|
63
src/components/client/shell/command/subcommand.ts
Normal file
63
src/components/client/shell/command/subcommand.ts
Normal 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
|
||||||
|
}
|
@ -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
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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; }
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user