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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@

**Important!** Some applications installed on the system need their node_modules directory to work and deleting them may break them. NPKILL will highlight them by displaying a :warning: to be careful.

## Search Mode

Search mode allows you to filter results. This can be particularly useful for limiting the view to a specific route or ensuring that only those results that meet the specified condition are “selected all.”

For example, you can use this expression to limit the results to those that are in the `work` directory and that include `data` somewhere in the path: `/work/.*/data`.

Press <kbd>/</kbd> to enter search mode. You can type a regex pattern to filter results.

Check notice on line 105 in README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

README.md#L105

Element: kbd

Press <kbd>Enter</kbd> to confirm the search and navigate the filtered results, or <kbd>Esc</kbd> to clear and exit.

Check notice on line 107 in README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

README.md#L107

Element: kbd

To exit from this mode, leave empty.

## Multi-Select Mode

This mode allows you to select and delete multiple folders at once, making it more efficient when cleaning up many directories.
Expand Down
7 changes: 7 additions & 0 deletions src/cli/cli.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ export class CliController {
);
this.uiResults.endNpkill$.subscribe(() => this.quit());
this.uiResults.goOptions$.subscribe(() => this.openOptions());
this.uiResults.search$.subscribe((state) => {
if (state === null) {
this.uiHeader.setSearch(null);
} else {
this.uiHeader.setSearch(state.text, state.isInputActive);
}
});

// Activate the main interactive component
this.activeComponent = this.uiResults;
Expand Down
33 changes: 33 additions & 0 deletions src/cli/ui/components/header/header.ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { MENU_BAR_OPTIONS } from './header-ui.constants.js';
export class HeaderUi extends BaseUi {
programVersion: string;
private activeMenuIndex = MENU_BAR_OPTIONS.DELETE;
private searchMode = false;
private searchText = '';
private isSearchInputActive = false;

readonly menuIndex$ = new BehaviorSubject<MENU_BAR_OPTIONS>(
MENU_BAR_OPTIONS.DELETE,
Expand All @@ -27,6 +30,19 @@ export class HeaderUi extends BaseUi {
});
}

setSearch(text: string | null, isInputActive = false) {
if (text === null) {
this.searchMode = false;
this.searchText = '';
this.isSearchInputActive = false;
} else {
this.searchMode = true;
this.searchText = text;
this.isSearchInputActive = isInputActive;
}
this.render();
}

render(): void {
// banner and tutorial
this.printAt(BANNER, UI_POSITIONS.INITIAL);
Expand Down Expand Up @@ -73,6 +89,23 @@ export class HeaderUi extends BaseUi {
}

private renderMenuBar(): void {
if (this.searchMode) {
let searchText = ` Search: ${this.searchText} `;
if (this.isSearchInputActive) {
searchText = ` Search: ${this.searchText}_ `;
this.printAt(pc.bgBlue(pc.white(searchText)), {
x: 2,
y: UI_POSITIONS.TUTORIAL_TIP.y,
});
} else {
this.printAt(pc.bgWhite(pc.black(searchText)), {
x: 2,
y: UI_POSITIONS.TUTORIAL_TIP.y,
});
}
return;
}

const options = Object.values(MENU_BAR);
let xStart = 2;
for (const option of options) {
Expand Down
1 change: 1 addition & 0 deletions src/cli/ui/components/help/help.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const HELP_SECTIONS: HelpSection[] = [
pc.cyan(pc.bold('Actions (normal mode)')),
` ${pc.green('SPACE / DEL')} Delete folder.`,
` ${pc.green('o')} Open parent folder.`,
` ${pc.green('/')} Search (Regex supported).`,
` ${pc.green('t')} Enter ${pc.green('multi-select mode')}.`,
'',
pc.cyan(pc.bold('Actions (multi-select mode)')),
Expand Down
125 changes: 102 additions & 23 deletions src/cli/ui/components/results.ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
readonly showDetails$ = new Subject<CliScanFoundFolder>();
readonly goOptions$ = new Subject<null>();
readonly endNpkill$ = new Subject<null>();
readonly search$ = new Subject<{
text: string;
isInputActive: boolean;
} | null>();

private isSearchInputMode = false;
private searchText = '';
private filteredResults: CliScanFoundFolder[] = [];

private readonly config: IConfig = DEFAULT_CONFIG;
private readonly KEYS = {
Expand Down Expand Up @@ -75,20 +83,23 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
}

private openFolder(): void {
const folder = this.resultsService.results[this.resultIndex];
const folder = this.results[this.resultIndex];
const parentPath = resolve(folder.path, '..');
this.openFolder$.next(parentPath);
}

private showDetails(): void {
const result = this.resultsService.results[this.resultIndex];
const result = this.results[this.resultIndex];
if (!result) {
return;
}
this.showDetails$.next(result);
}

private goOptions(): void {
if (this.searchText) {
return;
}
this.goOptions$.next(null);
}

Expand Down Expand Up @@ -120,7 +131,7 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
this.isRangeSelectionMode = true;
this.rangeSelectionStart = this.resultIndex;

const folder = this.resultsService.results[this.resultIndex];
const folder = this.results[this.resultIndex];
if (folder) {
if (this.selectedFolders.has(folder.path)) {
this.selectedFolders.delete(folder.path);
Expand All @@ -135,7 +146,7 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
return;
}

const allFolders = this.resultsService.results;
const allFolders = this.results;
const totalFolders = allFolders.length;
const selectedCount = this.selectedFolders.size;

Expand All @@ -160,7 +171,7 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
}

private toggleFolderSelection(): void {
const folder = this.resultsService.results[this.resultIndex];
const folder = this.results[this.resultIndex];
if (!folder) {
return;
}
Expand All @@ -184,15 +195,15 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
const start = Math.min(this.rangeSelectionStart, this.resultIndex);
const end = Math.max(this.rangeSelectionStart, this.resultIndex);

const firstFolder = this.resultsService.results[this.rangeSelectionStart];
const firstFolder = this.results[this.rangeSelectionStart];
if (!firstFolder) {
return;
}

const shouldSelect = this.selectedFolders.has(firstFolder.path);

for (let i = start; i <= end; i++) {
const folder = this.resultsService.results[i];
const folder = this.results[i];
if (!folder) {
continue;
}
Expand All @@ -217,8 +228,74 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
this.selectedFolders.clear();
}

onKeyInput({ name }: IKeyPress): void {
const action: (() => void) | undefined = this.KEYS[name];
private activateSearchInputMode(): void {
this.isSearchInputMode = true;
this.search$.next({ text: this.searchText, isInputActive: true });
this.render();
}

private handleSearchInput(key: IKeyPress): void {
if (key.name === 'return' || key.name === 'enter') {
this.isSearchInputMode = false;
if (this.searchText.trim() === '') {
this.searchText = '';
this.search$.next(null);
} else {
this.search$.next({ text: this.searchText, isInputActive: false });
}
this.render();
return;
}

if (key.name === 'backspace') {
this.searchText = this.searchText.slice(0, -1);
} else if (key.name === 'escape') {
this.isSearchInputMode = false;
this.searchText = '';
this.search$.next(null);
this.render();
return;
} else if (
key.sequence &&
key.sequence.length === 1 &&
!key.ctrl &&
!key.meta
) {
this.searchText += key.sequence;
} else {
return;
}

this.filterResults();
this.search$.next({ text: this.searchText, isInputActive: true });
this.resultIndex = 0;
this.scroll = 0;
this.render();
}

private filterResults(): void {
try {
const regex = new RegExp(this.searchText, 'i');
this.filteredResults = this.resultsService.results.filter((r) =>
regex.test(r.path),
);
} catch {
this.filteredResults = [];
}
}

onKeyInput(key: IKeyPress): void {
if (this.isSearchInputMode) {
this.handleSearchInput(key);
return;
}

if (key.sequence === '/') {
this.activateSearchInputMode();
return;
}

const action: (() => void) | undefined = this.KEYS[key.name];
if (action === undefined) {
return;
}
Expand All @@ -234,6 +311,8 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
return;
}

this.clear();

if (!this.haveResultsAfterCompleted) {
this.noResults();
return;
Expand Down Expand Up @@ -312,7 +391,8 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
private printFolderRow(folder: CliScanFoundFolder, row: number): void {
this.clearLine(row);
let { path, lastModification, size } = this.getFolderTexts(folder);
const isRowSelected = row === this.getRealCursorPosY();
const isRowSelected =
row === this.getRealCursorPosY() && !this.isSearchInputMode;

lastModification = isRowSelected
? pc.white(lastModification)
Expand Down Expand Up @@ -454,7 +534,7 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
}

cursorLastResult(): void {
this.moveCursor(this.resultsService.results.length - 1);
this.moveCursor(this.results.length - 1);
}

fitScroll(): void {
Expand All @@ -465,8 +545,7 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
const shouldScrollDown =
this.getRow(this.resultIndex) > this.terminal.rows + this.scroll - 2;

const isOnBotton =
this.resultIndex === this.resultsService.results.length - 1;
const isOnBotton = this.resultIndex === this.results.length - 1;

let scrollRequired = 0;

Expand All @@ -492,11 +571,7 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {

scrollFolderResults(scrollAmount: number): void {
const virtualFinalScroll = this.scroll + scrollAmount;
this.scroll = this.clamp(
virtualFinalScroll,
0,
this.resultsService.results.length,
);
this.scroll = this.clamp(virtualFinalScroll, 0, this.results.length);
this.clear();
}

Expand All @@ -511,7 +586,7 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {

// Lower limit
if (this.isCursorInUpperLimit()) {
this.resultIndex = this.resultsService.results.length - 1;
this.resultIndex = this.results.length - 1;
}

this.fitScroll();
Expand Down Expand Up @@ -590,7 +665,7 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
const SCROLLBAR_ACTIVE = pc.gray('█');
const SCROLLBAR_BG = pc.gray('░');

const totalResults = this.resultsService.results.length;
const totalResults = this.results.length;
const visibleRows = this.getRowsAvailable();

if (totalResults <= visibleRows) {
Expand Down Expand Up @@ -622,15 +697,15 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
}

private isCursorInUpperLimit(): boolean {
return this.resultIndex >= this.resultsService.results.length;
return this.resultIndex >= this.results.length;
}

private getRealCursorPosY(): number {
return this.getRow(this.resultIndex) - this.scroll;
}

private getVisibleScrollFolders(): CliScanFoundFolder[] {
return this.resultsService.results.slice(
return this.results.slice(
this.scroll,
this.getRowsAvailable() + this.scroll,
);
Expand All @@ -652,7 +727,7 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
}

private delete(): void {
const folder = this.resultsService.results[this.resultIndex];
const folder = this.results[this.resultIndex];
this.delete$.next(folder);
}

Expand All @@ -673,4 +748,8 @@ export class ResultsUi extends HeavyUi implements InteractiveUi {
private clamp(num: number, min: number, max: number): number {
return Math.min(Math.max(num, min), max);
}

private get results(): CliScanFoundFolder[] {
return this.searchText ? this.filteredResults : this.resultsService.results;
}
}
1 change: 1 addition & 0 deletions src/constants/cli.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ ${getHeader('How to interact')}
${pc.green('PgDown / Ctrl+d / d / l')} Move one page down.
${pc.green('Home, End')} Jump to first / last result.
${pc.green('o')} Open the parent directory.
${pc.green('/')} Search (Regex supported).
${pc.green('e')} Show errors.
${pc.green('q')} Quit.`;

Expand Down
Loading