Skip to content

Commit

Permalink
Add storage of command history
Browse files Browse the repository at this point in the history
  • Loading branch information
ianthomas23 committed Jul 12, 2024
1 parent 6631e4b commit 446e647
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 13 deletions.
13 changes: 11 additions & 2 deletions src/commands/builtin_command_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import { Context } from "../context"

export class BuiltinCommandRunner implements ICommandRunner {
names(): string[] {
return ["cd"]
return ["cd", "history"]
}

async run(cmdName: string, context: Context): Promise<void> {
switch (cmdName) {
case "cd":
this._cd(context)
break
}
case "history":
await this._history(context)
break
}
}

private _cd(context: Context) {
Expand All @@ -38,4 +41,10 @@ export class BuiltinCommandRunner implements ICommandRunner {
context.environment.set("OLDPWD", oldPwd)
context.environment.set("PWD", FS.cwd())
}

private async _history(context: Context) {
// TODO: support flags to clear, etc, history.
const { history, stdout } = context
await history.write(stdout)
}
}
10 changes: 4 additions & 6 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Environment } from "./environment"
import { History } from "./history"
import { IFileSystem } from "./file_system"
import { IInput, IOutput } from "./io"

Expand All @@ -10,16 +11,13 @@ export class Context {
readonly args: string[],
readonly fileSystem: IFileSystem,
readonly mountpoint: string,
environment: Environment,
readonly environment: Environment,
readonly history: History,
readonly stdin: IInput,
readonly stdout: IOutput,
) {
this.environment = environment
}
) {}

async flush(): Promise<void> {
await this.stdout.flush()
}

environment: Environment
}
55 changes: 55 additions & 0 deletions src/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { IOutput } from "./io/output"

/**
* Command history. Also maintains a current index in the history for scrolling through it.
*/
export class History {
add(command: string) {
this._current = null

if (!command.trim()) {
// Do nothing with command that is all whitespace.
return
}
if (this._history.length >= this._maxSize) {
this._history.shift()
}
this._history.push(command)
}

// Supports negative indexing from end.
at(index: number): string | null {
return this._history.at(index) ?? null
}

scrollCurrent(next: boolean): string | null {
if (next) {
this._current = (this._current === null) ? null : this._current+1
} else {
this._current = (this._current === null) ? this._history.length-1 : this._current-1
}

if (this._current !== null) {
if (this._current < 0) {
this._current = 0
} else if (this._current >= this._history.length) {
this._current = null
}
}

return this._current === null ? null : this.at(this._current)
}

async write(output: IOutput): Promise<void> {
for (let i = 0; i < this._history.length; i++) {
const index = String(i).padStart(5, ' ')
await output.write(`${index} ${this._history[i]}\n`)
}
}

private _history: string[] = []
private _current: number | null = null

// Smnall number for debug purposes. Should probably get this from an env var.
private _maxSize: number = 5
}
33 changes: 28 additions & 5 deletions src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommandRegistry } from "./command_registry"
import { Context } from "./context"
import { Environment } from "./environment"
import { IFileSystem } from "./file_system"
import { History } from "./history"
import { FileInput, FileOutput, IInput, IOutput, Pipe, TerminalInput, TerminalOutput } from "./io"
import { IOutputCallback } from "./output_callback"
import { CommandNode, PipeNode, parse } from "./parse"
Expand All @@ -16,12 +17,17 @@ export class Shell {
this._mountpoint = mountpoint;
this._currentLine = ""
this._environment = new Environment()
this._history = new History()
}

get environment(): Environment {
return this._environment
}

get history(): History {
return this._history
}

async input(char: string): Promise<void> {
// Might be a multi-char string if begins with escape code.
const code = char.charCodeAt(0)
Expand Down Expand Up @@ -57,10 +63,12 @@ export class Shell {
}
} else if (code == 27) { // Escape following by 1+ more characters
const remainder = char.slice(1)
if (remainder == "[A" || remainder == "[1A") { // Up arrow

} else if (remainder == "[B" || remainder == "[1B") { // Down arrow

if (remainder == "[A" || remainder == "[1A" || // Up arrow
remainder == "[B" || remainder == "[1B") { // Down arrow
const cmdText = this._history.scrollCurrent(remainder.endsWith("B"))
this._currentLine = cmdText !== null ? cmdText : ""
// Re-output whole line.
this.output(`\x1B[1K\r${this._environment.getPrompt()}${this._currentLine}`)
}
} else if (code == 4) { // EOT, usually = Ctrl-D

Expand Down Expand Up @@ -102,6 +110,20 @@ export class Shell {

// Keeping this public for tests.
async _runCommands(cmdText: string): Promise<void> {
if (cmdText.startsWith("!")) {
// Get command from history and run that.
const index = parseInt(cmdText.slice(1))
const possibleCmd = this._history.at(index)
if (possibleCmd === null) {
await this.output("\x1b[1;31m!" + index + ": event not found\x1b[1;0m\r\n")
await this.output(this._environment.getPrompt())
return
}
cmdText = possibleCmd
}

this._history.add(cmdText)

const stdin = new TerminalInput()
const stdout = new TerminalOutput(this._outputCallback)
try {
Expand Down Expand Up @@ -162,7 +184,7 @@ export class Shell {

const args = commandNode.suffix.map((token) => token.value)
const context = new Context(
args, this._fileSystem!, this._mountpoint, this._environment, input, output,
args, this._fileSystem!, this._mountpoint, this._environment, this._history, input, output,
)
await runner.run(name, context)

Expand All @@ -177,6 +199,7 @@ export class Shell {
private readonly _outputCallback: IOutputCallback
private _currentLine: string
private _environment: Environment
private _history: History

private _fsModule: any
private _fileSystem?: IFileSystem
Expand Down
49 changes: 49 additions & 0 deletions tests/history.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { shell_setup_empty } from "./shell_setup"

// Not accessing the history object directly, here using the Shell.

describe("history", () => {
it("should be stored", async () => {
const { shell, output } = await shell_setup_empty()

await shell._runCommands("cat")
await shell._runCommands("echo")
await shell._runCommands("ls")
output.clear()

await shell._runCommands("history")
expect(output.text).toEqual(" 0 cat\r\n 1 echo\r\n 2 ls\r\n 3 history\r\n")
})

it("should limit storage to max size", async () => {
// TODO: Max size initially hard-coded as 5, will change to use env var.
const { shell, output } = await shell_setup_empty()

await shell._runCommands("cat")
await shell._runCommands("echo")
await shell._runCommands("ls")
await shell._runCommands("uname")
await shell._runCommands("uniq")
output.clear()

await shell._runCommands("history")
expect(output.text).toEqual(
" 0 echo\r\n 1 ls\r\n 2 uname\r\n 3 uniq\r\n 4 history\r\n")
})

it("should rerun previous command using !index syntax", async () => {
const { shell, output } = await shell_setup_empty()

await shell._runCommands("cat")
await shell._runCommands("echo hello")
await shell._runCommands("ls")
output.clear()

await shell._runCommands("!1")
expect(output.text).toEqual("hello\r\n")
})

// Need ! out of bounds

// Need up and down to be tested.
})

0 comments on commit 446e647

Please sign in to comment.