Skip to content

Commit

Permalink
Refactor console
Browse files Browse the repository at this point in the history
  • Loading branch information
pindab0ter committed Apr 9, 2024
1 parent 57dad78 commit 0b8d9e0
Show file tree
Hide file tree
Showing 17 changed files with 302 additions and 293 deletions.
5 changes: 5 additions & 0 deletions assets/js/console/AutocompleteCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Command } from "./Command";

export abstract class AutocompleteCommand extends Command {
abstract autocomplete(arg: string): string[] | null;
}
6 changes: 6 additions & 0 deletions assets/js/console/Command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Console } from "./Console";

export interface Command {
readonly name: string;
execute(console: Console, args: string[]): void;
}
171 changes: 171 additions & 0 deletions assets/js/console/Console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Command } from "./Command";
import { getCommandFromInput, longestCommonPrefix } from "./helpers";
import { ChangeDirectory } from "./commands/ChangeDirectory";
import { Clear } from "./commands/Clear";
import { Help } from "./commands/Help";
import { Kitties } from "./commands/Kitties";
import { List } from "./commands/List";

export class Console {
private inputElement = document.getElementById("prompt-input") as HTMLSpanElement;
private promptBlur = document.getElementById("prompt-blur") as HTMLSpanElement;
private consoleElement = document.getElementById("console") as HTMLDivElement;
private nav = document.getElementsByTagName("nav")[0];

static commands: Command[] = [
new ChangeDirectory(),
new Clear(),
new Help(),
new Kitties(),
new List(),
];

public initialise(): void {
// Focus prompt input on page load for browsers that don't support autofocus on contenteditable elements.
this.inputElement.focus();

this.mirrorInputPromptToBlurredPrompt();
this.moveCaretToEndOnFocus();
this.listenForKeyboardInput();
this.focusPromptOnClick();
}

private mirrorInputPromptToBlurredPrompt(): void {
this.inputElement.addEventListener("input", () => {
this.setInput(this.inputElement.textContent.replace(/\s/g, "\xA0"));
});
}

private moveCaretToEndOnFocus(): void {
this.inputElement.addEventListener("focusin", () => {
this.moveCaretToEnd();
});
}

private listenForKeyboardInput(): void {
/**
* Handle enter key press to clear the prompt input.
*/
document.addEventListener("keydown", (event: KeyboardEvent) => {
const input = this.inputElement.textContent.replace(/\xA0/g, " ").trim();

switch (event.key) {
case "ArrowLeft":
case "ArrowRight":
case "ArrowUp":
case "ArrowDown":
event.preventDefault();
break;

// Clear prompt input
case "Enter":
event.preventDefault();
this.onEnter(input);
break;

case "Tab":
event.preventDefault();
this.onTab(input);
break;

// Remove focus from prompt input
case "Escape":
this.inputElement.blur();
break;
}
});
}

private focusPromptOnClick(): void {
/**
* Focus prompt input when clicking on the header.
*/
this.nav.addEventListener("click", (event: MouseEvent) => {
// Prevent focusing prompt input when clicking on a link.
if (event.target instanceof HTMLAnchorElement) {
return;
}

this.inputElement.focus();
});
}

private onEnter(input: string) {
const { command, args } = getCommandFromInput(input);
this.setInput();
this.clearOutput();

if (command) {
command.execute(this, args);
return;
}

this.print("Command not found. Type `help` for a list of available commands.");
return;
}

private onTab(input: string) {
if (input.length === 0) {
return;
}

const matchingCommands = Console.commands.filter((command: Command) =>
command.name.startsWith(input),
);

switch (matchingCommands.length) {
case 0:
return;

case 1:
const matchingCommand = matchingCommands[0];
if (input.length < matchingCommand.name.length) {
this.inputElement.textContent = matchingCommand.name;
this.promptBlur.textContent = matchingCommand.name;
this.moveCaretToEnd();
}
return;

default:
const matchingPrefix = longestCommonPrefix(
matchingCommands.map((command: Command) => command.name),
);
this.setInput("");
this.inputElement.textContent = matchingPrefix;
this.promptBlur.textContent = matchingPrefix;
this.moveCaretToEnd();
}
return;
}

private moveCaretToEnd() {
const range = document.createRange();
const selection = window.getSelection();

// Move caret to end of prompt input.
range?.setStart(this.inputElement, this.inputElement.childNodes.length);
range?.collapse(false);
selection?.removeAllRanges();
selection?.addRange(range);
}

public print(...lines: string[]) {
lines.forEach((line: string) => {
const outputElement = document.createElement("pre");
outputElement.textContent = line;
this.consoleElement.appendChild(outputElement);
});
}

public setInput(newInput: string = "") {
this.inputElement.textContent = newInput;
this.promptBlur.textContent = newInput;
this.moveCaretToEnd();
}

public clearOutput() {
while (this.consoleElement?.firstChild) {
this.consoleElement.removeChild(this.consoleElement.firstChild);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
import { Command } from "../Command";

import { getAllPages, getPagesInPath } from "../helpers";
import { getAllPages, getPagesInPath, slugPath } from "../helpers";
import { HugoPage } from "../../types/hugo";
import { Console } from "../Console";

export class ChangeDirectory implements Command {
public readonly name: string = "cd";
private readonly allPages: HugoPage[] = getAllPages();
private readonly pagesInPath: HugoPage[] = getPagesInPath(window.location.pathname);

public execute(consoleElement: HTMLDivElement, args: string[] = []): void {
public execute(console: Console, args: string[] = []): void {
if (args.length === 0) {
const outputElement = document.createElement("pre");
outputElement.textContent = "cd: missing argument";
consoleElement.appendChild(outputElement);
console.print("cd: missing argument");
return;
}

if (args.length > 1) {
const outputElement = document.createElement("pre");
outputElement.textContent = "cd: too many arguments";
consoleElement.appendChild(outputElement);
console.print("cd: too many arguments");
return;
}

const allPages = getAllPages();
const pagesInPath = getPagesInPath(window.location.pathname);

// Change to root directory
if (!args.length || ["/", "~"].includes(args[0])) {
window.location.pathname = "/";
Expand Down Expand Up @@ -55,26 +51,23 @@ export class ChangeDirectory implements Command {

// Change to an absolute path
if (inputPath.startsWith("/") || inputPath.startsWith("~/")) {
const page = allPages.find(
(p: HugoPage) =>
p.Path.toLowerCase() === inputPath ||
"/" + p.Section.toLowerCase() + "/" + p.Slug === inputPath,
const page = this.allPages.find(
(p: HugoPage) => p.Path.toLowerCase() === inputPath || slugPath(p) === inputPath,
);

if (page !== undefined) {
window.location.pathname = "/" + page.Section.toLowerCase() + "/" + page.Slug;
window.location.pathname = slugPath(page).concat("/");
return;
}
return;
}

// Change to a relative path
console.log(inputPath, pagesInPath);
if (
pagesInPath.find(
this.pagesInPath.find(
(p: HugoPage) =>
p.Path.replace(window.location.pathname, "").toLowerCase() === inputPath ||
p.Slug === inputPath,
slugPath(p).replace(window.location.pathname, "").toLowerCase() === inputPath,
)
) {
window.location.pathname = window.location.pathname.concat(inputPath).concat("/");
Expand Down
15 changes: 15 additions & 0 deletions assets/js/console/commands/Clear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Command } from "../Command";
import { Console } from "../Console";

export class Clear implements Command {
public readonly name: string = "clear";

public execute(console: Console, args: string[]): void {
if (args.length > 0) {
console.print("clear: too many arguments");
return;
}

console.clearOutput();
}
}
17 changes: 17 additions & 0 deletions assets/js/console/commands/Help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Command } from "../Command";
import { Console } from "../Console";

export class Help implements Command {
public readonly name: string = "help";

public execute(console: Console, args: string[]): void {
if (args.length > 0) {
console.print("help: too many arguments");
return;
}

const output = Console.commands.map((command: Command) => command.name).join(" ");

console.print(output);
}
}
16 changes: 16 additions & 0 deletions assets/js/console/commands/Kitties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Command } from "../Command";

import { commands, Console } from "../Console";

export class Kitties implements Command {
public readonly name: string = "kitties";

public execute(console: Console, args: string[]): void {
if (args.length > 0) {
console.print("kitties: too many arguments");
return;
}

window.location.href = "https://hamana.nl/";
}
}
35 changes: 35 additions & 0 deletions assets/js/console/commands/List.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Command } from "../Command";

import { HugoPage } from "../../types/hugo";
import { getPagesInPath } from "../helpers";
import { Console } from "../Console";

export class List implements Command {
public readonly name: string = "ls";

public execute(console: Console, args: string[]): void {
if (args.length > 0) {
console.print("ls: too many arguments");
return;
}

const currentPath = window.location.pathname;
let pages = getPagesInPath(currentPath);

console.print(".");

if (currentPath !== "/") {
console.print("..");
}

const paths = pages.map((page: HugoPage): string => {
if (page.Slug) {
return page.Slug.concat("/");
} else {
return page.Path.replace(currentPath, "").concat("/");
}
});

console.print(...paths);
}
}
20 changes: 20 additions & 0 deletions assets/js/shell/helpers.ts → assets/js/console/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @ts-ignore
import params from "@params";
import { HugoPage } from "../types/hugo";
import { Command } from "./Command";
import { Console } from "./Console";

export function getAllPages(): HugoPage[] {
const pages = JSON.parse(params.pages) as HugoPage[];
Expand Down Expand Up @@ -36,3 +38,21 @@ export function longestCommonPrefix(strings: string[]): string {

return firstString.substring(0, i);
}

export function slugPath(page: HugoPage): string {
return (page.Section + "/" + page.Slug + "/").toLowerCase();
}

export function getCommandFromInput(input: string): {
command: Command | null;
args: string[];
} {
if (input === "") return { command: null, args: [] };

const command: Command = Console.commands.find((command: Command) =>
input.startsWith(command.name),
);
const args = input.split(" ").slice(1) || [];

return { command, args };
}
7 changes: 5 additions & 2 deletions assets/js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
initialiseDarkModeToggleListener,
} from "./darkMode";
import smoothScrollToNode from "./smoothScrollToNode";
import initialisePrompt from "./shell/prompt";
import { Console } from "./console/Console";
import { CustomWindow } from "./types/main";

declare let window: CustomWindow;
Expand All @@ -17,4 +17,7 @@ initialiseDarkModeListener();
/** Called by the bouncing arrow on the home page. */
window.smoothScrollToNode = smoothScrollToNode;

initialisePrompt();
document.addEventListener("DOMContentLoaded", () => {
const prompt = new Console();
prompt.initialise();
});
4 changes: 0 additions & 4 deletions assets/js/shell/Command.ts

This file was deleted.

Loading

0 comments on commit 0b8d9e0

Please sign in to comment.