chore: make webfs usable

This commit is contained in:
binekrasik
2026-05-17 23:52:31 +02:00
parent f8335dcf5f
commit 4f4b72b915
14 changed files with 81 additions and 34 deletions

View File

@@ -4,11 +4,6 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- google fonts -->
<!--<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet" />-->
<!-- import main stylesheet --> <!-- import main stylesheet -->
<link rel="stylesheet" href="/src/styles/app.scss" /> <link rel="stylesheet" href="/src/styles/app.scss" />

View File

@@ -22,7 +22,7 @@ request.onerror = _ => {
"webfs error", "webfs error",
"Failed to initialize the webfs database using IndexedDB. Make sure webshell does not have any IndexedDB related permissions disabled and try again.", "Failed to initialize the webfs database using IndexedDB. Make sure webshell does not have any IndexedDB related permissions disabled and try again.",
'critical', 'critical',
).Show(-1) ).Show()
} }
request.onsuccess = event => { request.onsuccess = event => {

View File

@@ -212,8 +212,23 @@ export class Item {
private normalizePath(path: string): string { private normalizePath(path: string): string {
if (!path.startsWith('/')) path = '/' + path if (!path.startsWith('/')) path = '/' + path
if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1)
return path // Resolve . and .. segments.
// Split on '/' and walk each part: skip empty strings (from leading
// slash or consecutive slashes) and '.', pop on '..', push otherwise.
const parts = path.split('/')
const resolved: string[] = []
for (const part of parts) {
if (part === '' || part === '.') {
continue
} else if (part === '..') {
if (resolved.length > 0) resolved.pop() // never climbs above root
} else {
resolved.push(part)
}
}
return '/' + resolved.join('/')
} }
private getParentPath(): string { private getParentPath(): string {

View File

View File

@@ -3,10 +3,6 @@ import type { SimpleStream } from '../utils/SimpleStream'
import { Program } from './Program' import { Program } from './Program'
export class Cat extends Program { export class Cat extends Program {
constructor() {
super()
}
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, __: Item, args: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, __: Item, args: string[]): Promise<number> {
let item: Item = await Item.open(args[1]) let item: Item = await Item.open(args[1])

View File

@@ -7,17 +7,24 @@ export class Echo extends Program {
super() super()
} }
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, ___: Item, args: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> {
let item: Item = await Item.open(args[1]) if (args.length < 2) {
stdout.emit("echo: error: missing path argument\n")
return 1
}
const item = await Item.open(args[1].startsWith('/')
? args[1]
: `${workdir.GetPath()}${workdir.GetPath().endsWith('/') ? '' : '/'}${args[1]}`)
if (!(await item.Exists())) { if (!(await item.Exists())) {
stdout.emit(`echo: error: item ${item.GetPath()} doesn't exist.\n`) stdout.emit(`echo: error: item ${item.GetPath()} doesn't exist.\n`)
return 1 return 2
} }
if (item.IsDirectory()) { if (item.IsDirectory()) {
stdout.emit(`echo: error: can't write data to a directory; item ${item.GetPath()} is a directory.\n`) stdout.emit(`echo: error: can't write data to a directory; item ${item.GetPath()} is a directory.\n`)
return 2 return 3
} }
await item.Write(args.slice(2).join(' ')) await item.Write(args.slice(2).join(' '))

View File

@@ -8,7 +8,12 @@ export class Ls extends Program {
} }
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> {
let item: Item = args[1] ? await Item.openDir(args[1]) : workdir const item = args[1]
? await Item.openDir(args[1].startsWith('/')
? args[1]
: `${workdir.GetPath()}${workdir.GetPath().endsWith('/') ? '' : '/'}${args[1]}`)
: workdir
if (args[1] && !item.IsDirectory()) { if (args[1] && !item.IsDirectory()) {
stdout.emit("ls: error: the provided path is not a directory\n") stdout.emit("ls: error: the provided path is not a directory\n")
return 1 return 1
@@ -22,10 +27,11 @@ export class Ls extends Program {
console.log(item) console.log(item)
stdout.emit(`-> Listing contents of item: '${item.GetPath()}'\n`) stdout.emit(`-> Listing contents of item: '${item.GetPath()}'\n`)
stdout.emit(` [ attr name ]\n`) stdout.emit(` [ drwx name ]\n`)
const items = await item.List() const items = await item.List()
items.forEach((entry, i) => { items.forEach(entry => {
stdout.emit(` | ${''.padEnd(4, '-').padEnd(8, ' ')} ${entry.GetName()}\n`) stdout.emit(item.IsDirectory().toString())
stdout.emit(` | ${(item.IsDirectory() ? 'd' : '').padEnd(4, '-').padEnd(8, ' ')} ${entry.GetName()}\n`)
}) })
return 0 return 0

View File

@@ -8,12 +8,20 @@ export class Mkdir extends Program {
} }
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> {
const item = await Item.openDir(args[1]) if (args.length < 2) {
stdout.emit("mkdir: error: missing path argument\n")
return 1
}
const item = await Item.openDir(args[1].startsWith('/')
? args[1]
: `${workdir.GetPath()}${workdir.GetPath().endsWith('/') ? '' : '/'}${args[1]}`)
stdout.emit(`does ${args[1]} exist? ${await item.Exists()}\n`) stdout.emit(`does ${args[1]} exist? ${await item.Exists()}\n`)
if (await item.Exists()) { if (await item.Exists()) {
stdout.emit(`mkdir: directory ${item.GetPath()} already exists.\n`) stdout.emit(`mkdir: directory ${item.GetPath()} already exists.\n`)
return 1 return 2
} }
item.Create() item.Create()

View File

@@ -16,9 +16,13 @@ export class Rm extends Program {
let item: Item let item: Item
try { try {
item = await Item.openDir(args[1]) item = await Item.openDir(args[1].startsWith('/')
? args[1]
: `${workdir.GetPath()}${workdir.GetPath().endsWith('/') ? '' : '/'}${args[1]}`)
} catch { } catch {
item = await Item.open(args[1]) item = await Item.open(args[1].startsWith('/')
? args[1]
: `${workdir.GetPath()}${workdir.GetPath().endsWith('/') ? '' : '/'}${args[1]}`)
} }
if (!(await item.Exists())) { if (!(await item.Exists())) {

View File

@@ -10,8 +10,9 @@ export class Touch extends Program {
async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> { async Exec(_: SimpleStream<string>, stdout: SimpleStream<string>, workdir: Item, args: string[]): Promise<number> {
stdout.emit(`touching children, please wait...\n`) stdout.emit(`touching children, please wait...\n`)
const item = await Item.open(args[1]) const path = args.slice(1).join()
stdout.emit(`does ${args[1]} exist? ${await item.Exists()}\n`) const item = await Item.open(path.includes('/') ? path : `${workdir.GetPath()}${workdir.GetPath().endsWith('/') ? '' : '/'}${path}`)
stdout.emit(`does ${item.GetPath()} exist? ${await item.Exists()}\n`)
if (await item.Exists()) { if (await item.Exists()) {
stdout.emit("touch: the file already exists.\n") stdout.emit("touch: the file already exists.\n")

View File

@@ -1,3 +1,4 @@
import type { Item } from '../fs/Item'
import type { Program } from '../program/Program' import type { Program } from '../program/Program'
import type { Terminal } from '../terminal/Terminal' import type { Terminal } from '../terminal/Terminal'
import type { EventBroadcaster } from '../utils/EventBroadcaster' import type { EventBroadcaster } from '../utils/EventBroadcaster'
@@ -17,4 +18,5 @@ export abstract class Shell {
abstract ExecuteProgram(name: string, args: string[]): void abstract ExecuteProgram(name: string, args: string[]): void
abstract Init(): Promise<void> abstract Init(): Promise<void>
abstract HandleKeyInput(key: string, isCharacter: boolean): void abstract HandleKeyInput(key: string, isCharacter: boolean): void
abstract SetWorkingDirectory(directory: Item): Promise<void>
} }

View File

@@ -21,6 +21,8 @@ import { ResetIndexedDb } from '../program/ResetIndexedDb'
import { Cat } from '../program/Cat' import { Cat } from '../program/Cat'
import { Echo } from '../program/Echo' import { Echo } from '../program/Echo'
import { Mkdir } from '../program/Mkdir' import { Mkdir } from '../program/Mkdir'
import { Pwd } from '../program/Pwd'
import { Cd } from '../program/Cd'
export class Wush extends Shell { export class Wush extends Shell {
public readonly Version = "0.1.0" public readonly Version = "0.1.0"
@@ -47,7 +49,7 @@ export class Wush extends Shell {
readonly stdout: SimpleStream<string> readonly stdout: SimpleStream<string>
private programs: { [name: string]: Program } = {} private programs: { [name: string]: Program } = {}
private workingDirectory: Item = null as unknown as Item // workdir is initialized in the Init so this should be safe private workingDirectory: Item = null as unknown as Item // workdir is initialized in Init so this should be safe
constructor(broadcaster: EventBroadcaster, terminal: Terminal) { constructor(broadcaster: EventBroadcaster, terminal: Terminal) {
super(broadcaster, terminal) super(broadcaster, terminal)
@@ -65,9 +67,6 @@ export class Wush extends Shell {
// load workdir // load workdir
this.workingDirectory = await Item.Root() this.workingDirectory = await Item.Root()
if (!this.workingDirectory.Exists())
this.workingDirectory.Create()
// load core programs // load core programs
this.programs['clear'] = new Clear() this.programs['clear'] = new Clear()
this.programs['eval'] = new Eval() this.programs['eval'] = new Eval()
@@ -83,6 +82,8 @@ export class Wush extends Shell {
this.programs['cat'] = new Cat() this.programs['cat'] = new Cat()
this.programs['echo'] = new Echo() this.programs['echo'] = new Echo()
this.programs['mkdir'] = new Mkdir() this.programs['mkdir'] = new Mkdir()
this.programs['pwd'] = new Pwd()
this.programs['cd'] = new Cd(this)
this.stdout.on(data => this.WriteEscapedString(data)) this.stdout.on(data => this.WriteEscapedString(data))
this.Prompt() this.Prompt()
@@ -121,7 +122,19 @@ export class Wush extends Shell {
} }
Prompt() { Prompt() {
this.terminal.Write(`hi [${this.execExitCode}] -> `) this.terminal.Write(`boykisser in ${this.workingDirectory.GetPath()} -> `)
}
/**
* Changes the working directory
* @param directory the directory to enter into
* @throws an error if the directory cannot be opened
*/
async SetWorkingDirectory(directory: Item) {
if (!(await directory.Exists()) || !directory.IsDirectory())
throw new Error(`Directory ${directory.GetPath()} doesn't exist`)
this.workingDirectory = directory
} }
WriteStdin(data: string) { WriteStdin(data: string) {

View File

@@ -2,7 +2,7 @@
@use "./colors.scss" as colors; @use "./colors.scss" as colors;
body { body {
margin: 0; margin: 10px;
background: colors.$terminal-background; background: colors.$terminal-background;
} }

View File

@@ -155,8 +155,8 @@ export class Terminal {
UpdateCursor() { UpdateCursor() {
this.SetCursorStyle(this.cursorStyle) this.SetCursorStyle(this.cursorStyle)
this.cursor.style.left = `${this.cursorPosition.col * this.cellWidth}px` this.cursor.style.left = `${this.cursorPosition.col * this.cellWidth + this.terminal.offsetLeft}px `
this.cursor.style.top = `${this.cursorPosition.row * this.cellHeight}px` this.cursor.style.top = `${this.cursorPosition.row * this.cellHeight + this.terminal.offsetTop}px`
} }
GetCursorPosition(): CursorPosition { GetCursorPosition(): CursorPosition {