Skip to content

Commit

Permalink
Different ls output depending on terminal width
Browse files Browse the repository at this point in the history
  • Loading branch information
ianthomas23 committed May 24, 2024
1 parent 59f4994 commit 9d1961b
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 12 deletions.
90 changes: 82 additions & 8 deletions src/commands/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,95 @@ export class LsCommand extends Command<LsOptions> {
const args = context.args
const options = Options.fromArgs(args, LsOptions)

// Can use lines like this for options.
if (options.reverse.isSet) {

}

// Validate and expand arguments (flags and file/directory names).
// Only supporting single path and no flags so far.
if (args.length > 1) {
// Write error message to stderr
return 1
}

const path = args.length == 0 ? (context.env.get("PWD") ?? "/"): args[0]
const filenames = await context.filesystem.list(path)
await context.stdout.write(filenames.join(" ") + "\r\n") // How to deal with newlines?
const path = args.length == 0 ? (context.env_string("PWD") ?? "/"): args[0]
let filenames = await context.filesystem.list(path)

// Can use lines like this for options.
if (options.reverse.isSet) {

}

const width = context.env_number("COLUMNS")
const output = _toColumns(filenames, width)
await context.stdout.write(output) // How to deal with newlines?
return 0
}
}

function _range(n: number): number[] {
const range = new Array<number>(n)
for (let i = 0; i < n; ++i) {
range[i] = i
}
return range
}

function _toColumns(paths: string[], width: number | null): string {
function columnWidths(nCols: number): number[] {
return _range(nCols).map((col) => Math.max(...lengths.slice(col*nRows, (col+1)*nRows)))
}

function nColsFromRows(nRows: number): number {
return Math.ceil(lengths.length / nRows)
}

function singleColumn(): string {
return paths.join("\r\n") + "\r\n"
}

function singleRow(): string {
return paths.join(" ") + "\r\n"
}

function widthForRows(nRows: number): number {
const nCols = nColsFromRows(nRows)
return columnWidths(nCols).reduce((a, b) => a+b) + 2*nCols - 1
}

if (width == null || width < 1) {
return singleColumn()
}

const lengths = paths.map((f) => f.length)
if (lengths.reduce((a, b) => a+b) + 2*paths.length -1 <= width) {
return singleRow()
}

const max = Math.max(...lengths)
if (max + 1 >= width) {
return singleColumn()
}

// Increase the number of rows until the filenames fit in the required width.
// Can use min path length to find starting nRows > 2
let nRows = 2
while (widthForRows(nRows) > width) {
nRows++
}

const nCols = nColsFromRows(nRows)
const colWidths = columnWidths(nCols)

const range = _range(nCols)
let ret = ""
for (let row = 0; row < nRows; ++row) {
const indices = range.map((i) => row + i*nRows).filter((i) => i < paths.length)
for (let col = 0; col < indices.length; col++) {
const path = paths[indices[col]]
ret += path
if (col < indices.length - 1) {
ret += " ".repeat(colWidths[col] - path.length + 2)
}
}
ret += "\r\n"
}
// Probably want to return multiple rows, not a single string for all rows. And async.
return ret
}
12 changes: 12 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ export class Context {
await this.stdout.flush()
}

env_number(name: string): number | null {
const str = this.env_string(name)
if (str == null) {
return null
}
return Number(str)
}

env_string(name: string): string | null {
return this.env.get(name) ?? null
}

readonly args: string[]
readonly filesystem: IFileSystem
readonly stdout: Output
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/ls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("ls command", () => {
const exit_code = await cmd!.run(context)
expect(exit_code).toBe(0)

expect(spy).toHaveBeenCalledWith("dirA file1 file2\r\n")
expect(spy).toHaveBeenCalledWith("dirA\r\nfile1\r\nfile2\r\n")
spy.mockRestore()
})

Expand Down
6 changes: 3 additions & 3 deletions tests/shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ describe("Shell", () => {
const output = new MockTerminalOutput()
const shell = new Shell(fs, output.callback)
await shell._runCommands("ls")
expect(output.text).toEqual("dirA file1 file2\r\n")
expect(output.text).toEqual("dirA\r\nfile1\r\nfile2\r\n")
})

it("should run ls command with leading whitespace", async () => {
const fs = await file_system_setup("jupyter")
const output = new MockTerminalOutput()
const shell = new Shell(fs, output.callback)
await shell._runCommands(" ls")
expect(output.text).toEqual("dirA file1 file2\r\n")
expect(output.text).toEqual("dirA\r\nfile1\r\nfile2\r\n")
})

it("should run env command", async () => {
Expand Down Expand Up @@ -63,7 +63,7 @@ describe("Shell", () => {
const output = new MockTerminalOutput()
const shell = new Shell(fs, output.callback)
await shell.inputs(["l", "s", "\r"])
expect(output.text).toEqual("ls\r\ndirA file1 file2\r\n\x1b[1;31mjs-shell:$\x1b[1;0m ")
expect(output.text).toEqual("ls\r\ndirA\r\nfile1\r\nfile2\r\n\x1b[1;31mjs-shell:$\x1b[1;0m ")
})
})

Expand Down
59 changes: 59 additions & 0 deletions tests/shell_commands/shell.ls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ContentsManagerMock } from "@jupyterlab/services/lib/testutils"

import { MockTerminalOutput } from "../util"

import { JupyterFileSystem } from "../../src/jupyter_file_system"
import { Shell } from "../../src/shell"

export async function file_system_many_files(filenames: string[]): Promise<JupyterFileSystem> {
const cm = new ContentsManagerMock()
for (const filename of filenames) {
await cm.save(filename)
}
return new JupyterFileSystem(cm)
}

describe("Shell", () => {
describe("._runCommands", () => {
it("should run ls command", async () => {
const filenames = [
"a", "bb", "ccc", "dd", "eeeeeeeee", "f", "gg", "h", "iii", "j", "kkkkk",
]
const rows2 =
"a ccc eeeeeeeee gg iii kkkkk\r\n" +
"bb dd f h j\r\n"
const rows3 =
"a dd gg j\r\n" +
"bb eeeeeeeee h kkkkk\r\n" +
"ccc f iii\r\n"
const rows4 =
"a eeeeeeeee iii\r\n" +
"bb f j\r\n" +
"ccc gg kkkkk\r\n" +
"dd h\r\n"

const fs = await file_system_many_files(filenames)
const output = new MockTerminalOutput()
const shell = new Shell(fs, output.callback)

await shell._runCommands("ls")
expect(output.text).toEqual(filenames.join("\r\n") + "\r\n")
output.clear()

await shell.setSize(100, 45)
await shell._runCommands("ls")
expect(output.text).toEqual(rows2)
output.clear()

await shell.setSize(100, 28)
await shell._runCommands("ls")
expect(output.text).toEqual(rows3)
output.clear()

await shell.setSize(100, 23)
await shell._runCommands("ls")
expect(output.text).toEqual(rows4)
output.clear()
})
})
})

0 comments on commit 9d1961b

Please sign in to comment.