Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions site/src/bash/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { execute } from "./interpreter";
export { prompt } from "./prompt";
2 changes: 1 addition & 1 deletion site/src/bash/interpreter.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ async function executeAst(node, opts = {}) {
}
}

export default async function execute(input) {
export async function execute(input) {
const r = grammar.match(input);
if (!r.succeeded()) {
// Render error directly
Expand Down
11 changes: 11 additions & 0 deletions site/src/bash/prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import path from "../path";

// https://www.gnu.org/software/bash/manual/bash.html#Controlling-the-Prompt-1
export function prompt() {
let template = process.env.PS1 || "";
const r = { "\\u": process.env.USER, "\\w": path.getCwd() };
for (const [k, v] of Object.entries(r)) {
template = template.replace(k, v);
}
return template;
}
18 changes: 18 additions & 0 deletions site/src/builtins/cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import fs from "../fs";
import pathmodule from "../path";

export const cat = {
definition: { path: { type: "string", positional: 0 } },
cliOptions: {},
action: async ({ path }) => {
if (!path) return;
const fp = pathmodule.resolve(pathmodule.getCwd(), path);
const e = await fs.info(fp);
if (!e || e.type !== "file") {
const m = !e ? "not such file or directory" : "is a directory";
Cli.logger.error(m.concat(": ", path));
return process.exit(1);
}
return fs.readFile(fp).then(process.stdout.write);
},
};
18 changes: 18 additions & 0 deletions site/src/builtins/cd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import fs from "../fs.js";
import pathmodule from "../path.js";

export const cd = {
definition: { path: { type: "string", positional: 0 } },
cliOptions: {},
action: async ({ path }) => {
if (!path) return;
const fp = pathmodule.resolve(pathmodule.getCwd(), path);
const e = await fs.info(fp);
if (!e || e.type !== "directory") {
const m = !e ? "not such file or directory" : "not a directory";
Cli.logger.error(m.concat(": ", path));
return process.exit(1);
}
return pathmodule.setCwd(fp);
},
};
2 changes: 2 additions & 0 deletions site/src/builtins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from "./sleep";
export * from "./printenv";
export * from "./pwd";
export * from "./ls";
export * from "./cd";
export * from "./cat";
27 changes: 22 additions & 5 deletions site/src/builtins/ls.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import fs from "../fs.js";
import pathmodule from "../path.js";

export const ls = {
definition: {},
definition: { files: { type: "string", positional: true, default: [] } },
cliOptions: {},
action: async () => {
const cwd = fs.getCwd();
const contents = await fs.readDir(cwd);
process.stdout.write(contents.map((c) => c.name).join(" "));
action: async (params) => {
const rfiles = params.files.length ? params.files : ["."];
const fps = rfiles.map((f) => pathmodule.resolve(pathmodule.getCwd(), f));
const files = await Promise.all(fps.map((fp) => fs.info(fp).then((r) => [fp, r])));
// Log error for not-found locations
for (const f of files.filter((e) => !e[1])) {
Cli.logger.error("No such file or directory: ".concat(f[0]));
process.exitCode = 1;
}
// List contents for requested files
const efiles = files.filter((e) => e[1]).map((e) => ({ ...e[1], path: e[0] }));
for (let i = 0; i < efiles.length; i++) {
const f = efiles[i];
const contents = f.type == "file" ? [{ name: f.name }] : await fs.readDir(f.path);
// Include section title if type=directory & files>1
const title = f.type == "directory" && files.length > 1 ? f.path.concat(":\n") : "";
process.stdout.write(
"".concat(title, contents.map((c) => c.name).join(" "), i < efiles.length - 1 ? "\n\n" : ""),
);
}
},
};
4 changes: 2 additions & 2 deletions site/src/builtins/pwd.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "../fs";
import path from "../path";

export const pwd = {
definition: {},
cliOptions: {},
action: () => process.stdout.write(fs.getCwd()),
action: () => process.stdout.write(path.getCwd()),
};
21 changes: 12 additions & 9 deletions site/src/fs.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import kernel from "./kernel.js";
import pathmodule from "./path.js";

const root = await navigator.storage.getDirectory();

class FileSystem {
cwd = "/";
// Store a dedicated handle for stdin fd (stdout and stderr are sent to main thread) to be used in `readFileSync`
stdinHandle = null;
// Pending write operation
wp = Promise.resolve();

getCwd() {
return this.cwd;
}

setCwd(cwd) {
this.cwd = cwd;
}

async #getDirHandle(pathOrParts, create) {
const parts = Array.isArray(pathOrParts) ? pathOrParts : pathOrParts.split("/").filter(Boolean);
let h = root;
Expand Down Expand Up @@ -52,6 +44,17 @@ class FileSystem {
const h = await this.#getFileHandle(path);
return h.remove();
}

async info(path) {
const target = pathmodule.basename(path);
const parent = pathmodule.resolve(path, "..");
if (parent == path) {
return { type: "directory", name: path };
}
const dir = await this.readDir(parent);
return dir.find((e) => e.name === target);
}

async #readFile(path) {
const handle = await this.#getFileHandle(path);
return handle
Expand Down
23 changes: 20 additions & 3 deletions site/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import handleKey from "./key-handler.js";
import * as renderer from "./renderer.js";
import * as history from "./history.js";
import fs from "./fs.js";
import execute from "./bash/interpreter.js";
import path from "./path.js";
import { execute, prompt } from "./bash";
import * as builtincmds from "./builtins";
import kernel from "./kernel.js";
import "./index.css";
Expand All @@ -20,17 +21,22 @@ const blob = new Blob([blobSource], { type: "text/javascript" });
window.cliHandlerUrl = URL.createObjectURL(blob);
require("url").pathToFileURL = () => ({ href: cliHandlerUrl });

let [i, o, oa, lm] = ["input", "output", "output-after", "theme"].map((id) => document.getElementById(id));
let [i, sp, o, oa, lm] = ["input", "sprompt", "output", "output-after", "theme"].map((id) =>
document.getElementById(id),
);
document.addEventListener("click", () => i.focus());
o.addEventListener("click", (e) => e.stopPropagation());
lm.addEventListener("click", () =>
document.body.classList[document.body.classList.contains("light") ? "remove" : "add"]("light"),
);

// Initialize FS
await fs.init({ "/README.md": "Hello" });
await fs.init({ "/users/guest/README.md": "Welcome!" });
require("fs").readFileSync = fs.readFileSync.bind(fs);

// Initialize path
path.setCwd("/");

// Define `stdin.isTTY` as if kernel's fd=0's type is TTY
Object.defineProperty(process.stdin, "isTTY", {
get() {
Expand All @@ -42,8 +48,18 @@ Object.defineProperty(process.stdin, "isTTY", {
Object.assign(process.env, {
SHELL: "cliersh",
USER: "guest",
PS1: "\\u:\\w $",
});

// Method for updating bash prompt
const updatePrompt = () => {
let p = prompt();
if (window.CLI_PROMPT === p) return;
window.CLI_PROMPT = p;
sp.innerText = p;
};
updatePrompt();

handleKey(i, {
Enter: () => {
let inputValue = i.value;
Expand All @@ -52,6 +68,7 @@ handleKey(i, {
renderer.renderInput(inputValue);
i.value = "";
execute(inputValue).finally(() => {
updatePrompt();
renderer.flushOutput();
});
},
Expand Down
30 changes: 30 additions & 0 deletions site/src/path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class Path {
cwd = "/";

getCwd() {
return this.cwd;
}

async setCwd(path) {
this.cwd = path || "/";
}

resolve(...parts) {
const base = parts[0].replace(/\/*$/, "/");
return (
parts
.slice(1)
.reduce((acc, p) => new URL(p, acc), new URL("https://_" + base))
.pathname.replace(/\/*$/, "") || "/"
);
}

basename(path) {
if (path === "/") return "/";
return /[^\/]+$/.exec(path)?.[0];
}
}

const path = new Path();

export default path;
2 changes: 1 addition & 1 deletion site/src/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const OUTPUT_ID = "exec";
export function renderInput(value) {
const input = document.createElement("div");
input.className = "input-wrapper";
input.innerHTML = `$<span>${value}</span>`;
input.innerHTML = `${window.CLI_PROMPT}<span>${value}</span>`;
sp.classList.add("executing");
clearOutput(oa);
o.appendChild(input);
Expand Down