Skip to content

Commit f0f6fa0

Browse files
committed
core: Added shell auto-completions
1 parent db741ed commit f0f6fa0

4 files changed

Lines changed: 96 additions & 3 deletions

File tree

packages/apps/terminal/src/components/InputLine.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export function InputLine({ value, prefix, onChange, onKeyUp, onKeyDown, inputRe
2727
}, [value, inputRef]);
2828

2929
const checkCursorPosition = () => {
30-
console.log("Checking cursor position");
3130
const selectionStart = inputRef.current?.selectionStart;
3231
if (selectionStart != null) setCursorPosition(selectionStart);
3332
};

packages/apps/terminal/src/components/Terminal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ export function Terminal({ app, path: startPath, input, setTitle, close: exit, a
8989

9090
if (state.stream) return;
9191

92-
if (key === "Enter") {
92+
if (key === "Tab") {
93+
event.preventDefault();
94+
shell.autoComplete();
95+
} else if (key === "Enter") {
9396
const value = (event.target as HTMLInputElement).value;
9497
void shell.submitInput(value);
9598
setInputKey((previousKey) => previousKey + 1);

packages/core/src/features/shell/shell.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { proxy, ref } from "valtio";
22
import { Stream, StreamSignal } from "./stream";
33
import { CommandsManager } from "./commands";
4-
import { ANSI, Ansi, clamp, removeFromArray, Vector2 } from "@prozilla-os/shared";
4+
import { ANSI, Ansi, clamp, getLongestCommonPrefix, removeFromArray, Vector2 } from "@prozilla-os/shared";
55
import { EXIT_CODE, HOSTNAME, USERNAME, WELCOME_MESSAGE } from "../../constants/shell.const";
66
import { App } from "../apps/app";
77
import { VirtualFolder, VirtualRoot } from "../virtual-drive";
@@ -416,6 +416,81 @@ export class Shell {
416416
this.updatePrefix();
417417
}
418418

419+
/**
420+
* Provides completions based on the current input value.
421+
*/
422+
getCompletions() {
423+
const words = this.state.inputValue.split(" ");
424+
const lastWord = words[words.length - 1] ?? "";
425+
const isFirstWord = words.length <= 1;
426+
427+
let completions: string[] = [];
428+
429+
if (isFirstWord && !lastWord.includes("/")) {
430+
completions = CommandsManager.COMMANDS
431+
.filter((command) => command.name.startsWith(lastWord))
432+
.map((command) => command.name);
433+
}
434+
435+
if (!isFirstWord || lastWord.includes("/") || lastWord.startsWith(".")) {
436+
const pathParts = lastWord.split("/");
437+
const searchTerm = pathParts.pop() ?? "";
438+
const path = pathParts.join("/") || (lastWord.startsWith("/") ? "/" : ".");
439+
440+
const directory = this.state.workingDirectory.navigateToFolder(path);
441+
if (directory) {
442+
const entries = [...directory.getSubFolders(true), ...directory.getFiles(true)];
443+
const pathCompletions = entries
444+
.map((entry) => entry.name + (entry.isFolder() ? "/" : ""))
445+
.filter((name) => name.startsWith(searchTerm));
446+
447+
completions = [...completions, ...pathCompletions];
448+
}
449+
}
450+
451+
return completions;
452+
}
453+
454+
/**
455+
* Auto-completes the current input value or displays possible completions.
456+
*/
457+
autoComplete() {
458+
const completions = this.getCompletions();
459+
if (!completions.length) return;
460+
461+
const parts = this.state.inputValue.split(" ");
462+
const lastWord = parts.pop() ?? "";
463+
const commonPrefix = getLongestCommonPrefix(completions);
464+
465+
// If there's a common prefix longer than the current input, complete it
466+
const pathParts = lastWord.split("/");
467+
const searchTerm = pathParts.pop() ?? "";
468+
469+
if (commonPrefix.length > searchTerm.length) {
470+
pathParts.push(commonPrefix);
471+
parts.push(pathParts.join("/"));
472+
this.setInputValue(parts.join(" "));
473+
return;
474+
}
475+
476+
// Otherwise, handle single match or multiple options
477+
if (completions.length === 1) {
478+
pathParts.push(completions[0]);
479+
parts.push(pathParts.join("/"));
480+
this.setInputValue(parts.join(" "));
481+
} else {
482+
this.pushHistory({
483+
text: this.state.prefix + this.state.inputValue,
484+
isInput: true,
485+
value: this.state.inputValue,
486+
});
487+
this.pushHistory({
488+
text: completions.join(" "),
489+
isInput: false,
490+
});
491+
}
492+
}
493+
419494
static parseCommand(input: string): string[] {
420495
return input.match(/(?:[^\s"']+|"(?:[^"\\]|\\.)*"|'[^']*')+/g) ?? [];
421496
}

packages/shared/src/features/_utils/string.utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,20 @@
44
*/
55
export function parseBool(input: string) {
66
return input.trim().toLowerCase() === "true";
7+
}
8+
9+
/**
10+
* Returns the longest common prefix from a list of strings.
11+
*/
12+
export function getLongestCommonPrefix(strings: string[]) {
13+
if (!strings.length) return "";
14+
15+
let prefix = strings[0];
16+
for (let i = 1; i < strings.length; i++) {
17+
while (strings[i].indexOf(prefix) !== 0) {
18+
prefix = prefix.substring(0, prefix.length - 1);
19+
if (prefix === "") return "";
20+
}
21+
}
22+
return prefix;
723
}

0 commit comments

Comments
 (0)