Comparing version 0.10.1-1 to 0.11.0-0
@@ -18,5 +18,5 @@ export const OPTIONS = [ | ||
{ | ||
arg: ['-e', '--show-errors'], | ||
description: 'Show error messages if any.', | ||
name: 'show-errors', | ||
arg: ['-e', '--hide-errors'], | ||
description: 'Hide errors if any.', | ||
name: 'hide-errors', | ||
}, | ||
@@ -71,8 +71,9 @@ { | ||
┌------ CONTROLS -------------------- | ||
| SPACE: delete selected result | ||
| Cursor UP, k: move up | ||
| Cursor DOWN, j: move down | ||
| h, d, Ctrl+d, PgUp: move one page down | ||
| l, u, Ctrl+u, PgDown: move one page up | ||
| home, end: move to the first and last result`; | ||
| SPACE: delete selected result | ||
| Cursor UP, k: move up | ||
| Cursor DOWN, j: move down | ||
| h, d, Ctrl+d, PgUp: move one page down | ||
| l, u, Ctrl+u, PgDown: move one page up | ||
| home, end: move to the first and last result | ||
| e: show errors popup, next page`; | ||
export const HELP_FOOTER = 'Not all node_modules are bad! Some applications (like vscode, Discord, etc) need those dependencies to work. If their directory is deleted, the application will probably break (until the dependencies are reinstalled). NPKILL will show you these directories by highlighting them ⚠️'; | ||
@@ -79,0 +80,0 @@ export const COLORS = { |
@@ -16,3 +16,3 @@ export const MIN_CLI_COLUMNS_SIZE = 60; | ||
maxSimultaneousSearch: 6, | ||
showErrors: false, | ||
showErrors: true, | ||
sortBy: '', | ||
@@ -40,19 +40,21 @@ targetFolder: 'node_modules', | ||
TOTAL_SPACE: { x: 50, y: 3 }, | ||
ERRORS_COUNT: { x: 50, y: 2 }, | ||
TUTORIAL_TIP: { x: 4, y: 7 }, | ||
}; | ||
export const VALID_KEYS = [ | ||
'up', | ||
'down', | ||
'space', | ||
'j', | ||
'k', | ||
'h', | ||
'l', | ||
'u', | ||
'd', | ||
'pageup', | ||
'pagedown', | ||
'home', | ||
'end', // Move to the last result | ||
]; | ||
// export const VALID_KEYS: string[] = [ | ||
// 'up', // Move up | ||
// 'down', // Move down | ||
// 'space', // Delete | ||
// 'j', // Move down | ||
// 'k', // Move up | ||
// 'h', // Move page down | ||
// 'l', // Move page up | ||
// 'u', // Move page up | ||
// 'd', // Move page down | ||
// 'pageup', | ||
// 'pagedown', | ||
// 'home', // Move to the first result | ||
// 'end', // Move to the last result | ||
// 'e', // Show errors | ||
// ]; | ||
export const BANNER = `----- __ .__.__ .__ | ||
@@ -59,0 +61,0 @@ - ____ ______ | | _|__| | | | |
@@ -1,15 +0,20 @@ | ||
import { BANNER, DECIMALS_SIZE, DEFAULT_CONFIG, DEFAULT_SIZE, MARGINS, MIN_CLI_COLUMNS_SIZE, OVERFLOW_CUT_FROM, UI_HELP, UI_POSITIONS, VALID_KEYS, } from './constants/index.js'; | ||
import { COLORS, HELP_HEADER, HELP_FOOTER, OPTIONS, } from './constants/cli.constants.js'; | ||
import { ERROR_MSG, HELP_MSGS, INFO_MSGS, } from './constants/messages.constants.js'; | ||
import { Subject, from, interval } from 'rxjs'; | ||
import { SPINNERS, SPINNER_INTERVAL } from './constants/spinner.constants.js'; | ||
import { catchError, filter, map, mergeMap, switchMap, takeUntil, tap, } from 'rxjs/operators'; | ||
import { DEFAULT_CONFIG, MIN_CLI_COLUMNS_SIZE, UI_POSITIONS, } from './constants/index.js'; | ||
import { ERROR_MSG, INFO_MSGS } from './constants/messages.constants.js'; | ||
import { from } from 'rxjs'; | ||
import { catchError, filter, map, mergeMap, switchMap, tap, } from 'rxjs/operators'; | ||
import { COLORS } from './constants/cli.constants.js'; | ||
import { FOLDER_SORT } from './constants/sort.result.js'; | ||
import ansiEscapes from 'ansi-escapes'; | ||
import { GeneralUi } from './ui/general.ui.js'; | ||
import { HeaderUi } from './ui/header/header.ui.js'; | ||
import { HelpUi } from './ui/help.ui.js'; | ||
import { LogsUi } from './ui/logs.ui.js'; | ||
import { ResultsUi } from './ui/results.ui.js'; | ||
import { StatsUi } from './ui/header/stats.ui.js'; | ||
import { StatusUi } from './ui/header/status.ui.js'; | ||
import __dirname from './dirname.js'; | ||
import colors from 'colors'; | ||
import { homedir } from 'os'; | ||
import colors from 'colors'; | ||
import keypress from 'keypress'; | ||
import __dirname from './dirname.js'; | ||
import path from 'path'; | ||
export class Controller { | ||
logger; | ||
fileService; | ||
@@ -20,32 +25,17 @@ spinnerService; | ||
resultsService; | ||
uiService; | ||
folderRoot = ''; | ||
stdin = process.stdin; | ||
stdout = process.stdout; | ||
config = DEFAULT_CONFIG; | ||
cursorPosY = MARGINS.ROW_RESULTS_START; | ||
previusCursorPosY = MARGINS.ROW_RESULTS_START; | ||
scroll = 0; | ||
searchStart; | ||
searchDuration; | ||
finishSearching$ = new Subject(); | ||
KEYS = { | ||
up: this.moveCursorUp.bind(this), | ||
// tslint:disable-next-line: object-literal-sort-keys | ||
down: this.moveCursorDown.bind(this), | ||
space: this.delete.bind(this), | ||
j: this.moveCursorDown.bind(this), | ||
k: this.moveCursorUp.bind(this), | ||
h: this.moveCursorPageDown.bind(this), | ||
l: this.moveCursorPageUp.bind(this), | ||
d: this.moveCursorPageDown.bind(this), | ||
u: this.moveCursorPageUp.bind(this), | ||
pageup: this.moveCursorPageUp.bind(this), | ||
pagedown: this.moveCursorPageDown.bind(this), | ||
home: this.moveCursorFirstResult.bind(this), | ||
end: this.moveCursorLastResult.bind(this), | ||
execute(command, params) { | ||
return this[command](params); | ||
}, | ||
}; | ||
constructor(fileService, spinnerService, consoleService, updateService, resultsService) { | ||
uiHeader; | ||
uiGeneral; | ||
uiStats; | ||
uiStatus; | ||
uiResults; | ||
uiLogs; | ||
activeComponent; | ||
constructor(logger, fileService, spinnerService, consoleService, updateService, resultsService, uiService) { | ||
this.logger = logger; | ||
this.fileService = fileService; | ||
@@ -56,10 +46,16 @@ this.spinnerService = spinnerService; | ||
this.resultsService = resultsService; | ||
this.uiService = uiService; | ||
} | ||
init() { | ||
keypress(process.stdin); | ||
this.setErrorEvents(); | ||
this.getArguments(); | ||
this.logger.info(process.argv.join(' ')); | ||
this.logger.info(`Npkill started! v${this.getVersion()}`); | ||
this.initUi(); | ||
if (this.consoleService.isRunningBuild()) { | ||
this.uiHeader.programVersion = this.getVersion(); | ||
} | ||
this.consoleService.startListenKeyEvents(); | ||
this.parseArguments(); | ||
this.prepareScreen(); | ||
this.setupEventsListener(); | ||
this.initializeLoadingStatus(); | ||
this.uiStatus.start(); | ||
if (this.config.checkUpdates) | ||
@@ -69,3 +65,23 @@ this.checkVersion(); | ||
} | ||
getArguments() { | ||
initUi() { | ||
this.uiHeader = new HeaderUi(); | ||
this.uiService.add(this.uiHeader); | ||
this.uiResults = new ResultsUi(this.resultsService, this.consoleService, this.fileService); | ||
this.uiService.add(this.uiResults); | ||
this.uiStats = new StatsUi(this.config, this.resultsService, this.logger); | ||
this.uiService.add(this.uiStats); | ||
this.uiStatus = new StatusUi(this.spinnerService); | ||
this.uiService.add(this.uiStatus); | ||
this.uiGeneral = new GeneralUi(); | ||
this.uiService.add(this.uiGeneral); | ||
this.uiLogs = new LogsUi(this.logger); | ||
this.uiService.add(this.uiLogs); | ||
// Set Events | ||
this.uiResults.delete$.subscribe((folder) => this.deleteFolder(folder)); | ||
this.uiResults.showErrors$.subscribe(() => this.showErrorPopup(true)); | ||
this.uiLogs.close$.subscribe(() => this.showErrorPopup(false)); | ||
// Activate the main interactive component | ||
this.activeComponent = this.uiResults; | ||
} | ||
parseArguments() { | ||
const options = this.consoleService.getParameters(process.argv); | ||
@@ -86,4 +102,3 @@ if (options['help']) { | ||
if (!this.isValidSortParam(options['sort-by'])) { | ||
this.print(INFO_MSGS.NO_VALID_SORT_NAME); | ||
process.exit(); | ||
this.invalidSortParam(); | ||
} | ||
@@ -103,5 +118,5 @@ this.config.sortBy = options['sort-by']; | ||
if (options['full-scan']) | ||
this.folderRoot = this.getUserHomePath(); | ||
if (options['show-errors']) | ||
this.config.showErrors = true; | ||
this.folderRoot = homedir(); | ||
if (options['hide-errors']) | ||
this.config.showErrors = false; | ||
if (options['gb']) | ||
@@ -120,31 +135,30 @@ this.config.folderSizeInGB = true; | ||
} | ||
showErrorPopup(visible) { | ||
this.uiLogs.setVisible(visible); | ||
// Need convert to pattern and have a stack for recover latest | ||
// component. | ||
this.uiResults.freezed = visible; | ||
this.uiStats.freezed = visible; | ||
this.uiStatus.freezed = visible; | ||
if (visible) { | ||
this.activeComponent = this.uiLogs; | ||
this.uiLogs.render(); | ||
} | ||
else { | ||
this.activeComponent = this.uiResults; | ||
this.uiService.renderAll(); | ||
} | ||
} | ||
invalidSortParam() { | ||
this.uiService.print(INFO_MSGS.NO_VALID_SORT_NAME); | ||
process.exit(); | ||
} | ||
showHelp() { | ||
this.clear(); | ||
this.print(colors.inverse(INFO_MSGS.HELP_TITLE + '\n\n')); | ||
this.print(HELP_HEADER + '\n\n'); | ||
let lineCount = 0; | ||
OPTIONS.map((option, index) => { | ||
this.printAtHelp(option.arg.reduce((text, arg) => text + ', ' + arg), { | ||
x: UI_HELP.X_COMMAND_OFFSET, | ||
y: index + UI_HELP.Y_OFFSET + lineCount, | ||
}); | ||
const description = this.consoleService.splitWordsByWidth(option.description, this.stdout.columns - UI_HELP.X_DESCRIPTION_OFFSET); | ||
description.map((line) => { | ||
this.printAtHelp(line, { | ||
x: UI_HELP.X_DESCRIPTION_OFFSET, | ||
y: index + UI_HELP.Y_OFFSET + lineCount, | ||
}); | ||
++lineCount; | ||
}); | ||
}); | ||
this.printAt(HELP_FOOTER + '\n', { | ||
x: 0, | ||
y: lineCount * 2 + 2, | ||
}); | ||
new HelpUi(this.consoleService).show(); | ||
} | ||
showProgramVersion() { | ||
this.print('v' + this.getVersion()); | ||
this.uiService.print('v' + this.getVersion()); | ||
} | ||
showObsoleteMessage() { | ||
this.print(INFO_MSGS.DISABLED); | ||
this.uiService.print(INFO_MSGS.DISABLED); | ||
} | ||
@@ -166,22 +180,17 @@ setColor(color) { | ||
} | ||
clear() { | ||
this.print(ansiEscapes.clearTerminal); | ||
} | ||
print(text) { | ||
process.stdout.write.bind(process.stdout)(text); | ||
} | ||
prepareScreen() { | ||
this.checkScreenRequirements(); | ||
this.setRawMode(); | ||
this.clear(); | ||
this.printUI(); | ||
this.hideCursor(); | ||
this.uiService.setRawMode(); | ||
this.uiService.prepareUi(); | ||
this.uiService.setCursorVisible(false); | ||
this.uiService.clear(); | ||
this.uiService.renderAll(); | ||
} | ||
checkScreenRequirements() { | ||
if (this.isTerminalTooSmall()) { | ||
this.print(INFO_MSGS.MIN_CLI_CLOMUNS); | ||
this.uiService.print(INFO_MSGS.MIN_CLI_CLOMUNS); | ||
process.exit(); | ||
} | ||
if (!this.stdout.isTTY) { | ||
this.print(INFO_MSGS.NO_TTY); | ||
this.uiService.print(INFO_MSGS.NO_TTY); | ||
process.exit(); | ||
@@ -191,16 +200,22 @@ } | ||
checkVersion() { | ||
this.logger.info('Checking updates...'); | ||
this.updateService | ||
.isUpdated(this.getVersion()) | ||
.then((isUpdated) => { | ||
if (!isUpdated) | ||
this.showNewInfoMessage(); | ||
if (isUpdated) { | ||
this.showUpdateMessage(); | ||
this.logger.info('New version found!'); | ||
} | ||
else { | ||
this.logger.info('Npkill is update'); | ||
} | ||
}) | ||
.catch((err) => { | ||
const errorMessage = ERROR_MSG.CANT_GET_REMOTE_VERSION + ': ' + err.message; | ||
this.printError(errorMessage); | ||
this.newError(errorMessage); | ||
}); | ||
} | ||
showNewInfoMessage() { | ||
showUpdateMessage() { | ||
const message = colors.magenta(INFO_MSGS.NEW_UPDATE_FOUND); | ||
this.printAt(message, UI_POSITIONS.NEW_UPDATE_FOUND); | ||
this.uiService.printAt(message, UI_POSITIONS.NEW_UPDATE_FOUND); | ||
} | ||
@@ -210,177 +225,7 @@ isTerminalTooSmall() { | ||
} | ||
setRawMode(set = true) { | ||
this.stdin.setRawMode(set); | ||
process.stdin.resume(); | ||
} | ||
printUI() { | ||
/////////////////////////// | ||
// banner and tutorial | ||
this.printAt(BANNER, UI_POSITIONS.INITIAL); | ||
this.printAt(colors.yellow(colors.inverse(HELP_MSGS.BASIC_USAGE)), UI_POSITIONS.TUTORIAL_TIP); | ||
if (this.consoleService.isRunningBuild()) { | ||
this.printAt(colors.gray(this.getVersion()), UI_POSITIONS.VERSION); | ||
} | ||
/////////////////////////// | ||
// Columns headers | ||
this.printAt(colors.gray(INFO_MSGS.HEADER_COLUMNS), { | ||
x: this.stdout.columns - INFO_MSGS.HEADER_COLUMNS.length - 4, | ||
y: UI_POSITIONS.FOLDER_SIZE_HEADER.y, | ||
}); | ||
/////////////////////////// | ||
// npkill stats | ||
this.printAt(colors.gray(INFO_MSGS.TOTAL_SPACE + DEFAULT_SIZE), UI_POSITIONS.TOTAL_SPACE); | ||
this.printAt(colors.gray(INFO_MSGS.SPACE_RELEASED + DEFAULT_SIZE), UI_POSITIONS.SPACE_RELEASED); | ||
} | ||
printAt(message, position) { | ||
this.setCursorAt(position); | ||
this.print(message); | ||
} | ||
setCursorAt({ x, y }) { | ||
this.print(ansiEscapes.cursorTo(x, y)); | ||
} | ||
printAtHelp(message, position) { | ||
this.setCursorAtHelp(position); | ||
this.print(message); | ||
if (!/-[a-zA-Z]/.test(message.substring(0, 2)) && message !== '') { | ||
this.print('\n\n'); | ||
} | ||
} | ||
setCursorAtHelp({ x, y }) { | ||
this.print(ansiEscapes.cursorTo(x)); | ||
} | ||
initializeLoadingStatus() { | ||
this.spinnerService.setSpinner(SPINNERS.W10); | ||
interval(SPINNER_INTERVAL) | ||
.pipe(takeUntil(this.finishSearching$)) | ||
.subscribe(() => this.updateStatus(INFO_MSGS.SEARCHING + this.spinnerService.nextFrame()), (error) => this.printError(error)); | ||
} | ||
updateStatus(text) { | ||
this.printAt(text, UI_POSITIONS.STATUS); | ||
} | ||
printFoldersSection() { | ||
if (this.resultsService.noResultsAfterCompleted) { | ||
this.printNoResults(); | ||
return; | ||
} | ||
const visibleFolders = this.getVisibleScrollFolders(); | ||
this.clearLine(this.previusCursorPosY); | ||
visibleFolders.map((folder, index) => { | ||
const folderRow = MARGINS.ROW_RESULTS_START + index; | ||
this.printFolderRow(folder, folderRow); | ||
}); | ||
this.uiResults.render(); | ||
} | ||
printNoResults() { | ||
const message = `No ${colors[DEFAULT_CONFIG.warningColor](this.config.targetFolder)} found!`; | ||
this.printAt(message, { | ||
x: Math.floor(this.stdout.columns / 2 - message.length / 2), | ||
y: MARGINS.ROW_RESULTS_START + 2, | ||
}); | ||
} | ||
printFolderRow(folder, row) { | ||
let { path, lastModification, size } = this.getFolderTexts(folder); | ||
const isRowSelected = row === this.getRealCursorPosY(); | ||
lastModification = colors.gray(lastModification); | ||
if (isRowSelected) { | ||
path = colors[this.config.backgroundColor](path); | ||
size = colors[this.config.backgroundColor](size); | ||
lastModification = colors[this.config.backgroundColor](lastModification); | ||
this.paintBgRow(row); | ||
} | ||
if (folder.isDangerous) | ||
path = colors[DEFAULT_CONFIG.warningColor](path + '⚠️'); | ||
this.printAt(path, { | ||
x: MARGINS.FOLDER_COLUMN_START, | ||
y: row, | ||
}); | ||
this.printAt(lastModification, { | ||
x: this.stdout.columns - MARGINS.FOLDER_SIZE_COLUMN - 6, | ||
y: row, | ||
}); | ||
this.printAt(size, { | ||
x: this.stdout.columns - MARGINS.FOLDER_SIZE_COLUMN, | ||
y: row, | ||
}); | ||
} | ||
getFolderTexts(folder) { | ||
const folderText = this.getFolderPathText(folder); | ||
let folderSize = `${folder.size.toFixed(DECIMALS_SIZE)} GB`; | ||
let daysSinceLastModification = folder.modificationTime !== null && folder.modificationTime > 0 | ||
? Math.floor((new Date().getTime() / 1000 - folder.modificationTime) / 86400) + 'd' | ||
: '--'; | ||
if (folder.isDangerous) | ||
daysSinceLastModification = 'xxx'; | ||
// Align to right | ||
const alignMargin = 4 - daysSinceLastModification.length; | ||
daysSinceLastModification = | ||
' '.repeat(alignMargin > 0 ? alignMargin : 0) + daysSinceLastModification; | ||
if (!this.config.folderSizeInGB) { | ||
const size = this.fileService.convertGBToMB(folder.size); | ||
folderSize = `${size.toFixed(DECIMALS_SIZE)} MB`; | ||
} | ||
const folderSizeText = folder.size ? folderSize : '--'; | ||
return { | ||
path: folderText, | ||
size: folderSizeText, | ||
lastModification: daysSinceLastModification, | ||
}; | ||
} | ||
paintBgRow(row) { | ||
const startPaint = MARGINS.FOLDER_COLUMN_START; | ||
const endPaint = this.stdout.columns - MARGINS.FOLDER_SIZE_COLUMN; | ||
let paintSpaces = ''; | ||
for (let i = startPaint; i < endPaint; ++i) { | ||
paintSpaces += ' '; | ||
} | ||
this.printAt(colors[this.config.backgroundColor](paintSpaces), { | ||
x: startPaint, | ||
y: row, | ||
}); | ||
} | ||
getFolderPathText(folder) { | ||
let cutFrom = OVERFLOW_CUT_FROM; | ||
let text = folder.path; | ||
const ACTIONS = { | ||
// tslint:disable-next-line: object-literal-key-quotes | ||
deleted: () => { | ||
cutFrom += INFO_MSGS.DELETED_FOLDER.length; | ||
text = INFO_MSGS.DELETED_FOLDER + text; | ||
}, | ||
// tslint:disable-next-line: object-literal-key-quotes | ||
deleting: () => { | ||
cutFrom += INFO_MSGS.DELETING_FOLDER.length; | ||
text = INFO_MSGS.DELETING_FOLDER + text; | ||
}, | ||
'error-deleting': () => { | ||
cutFrom += INFO_MSGS.ERROR_DELETING_FOLDER.length; | ||
text = INFO_MSGS.ERROR_DELETING_FOLDER + text; | ||
}, | ||
}; | ||
if (ACTIONS[folder.status]) | ||
ACTIONS[folder.status](); | ||
text = this.consoleService.shortenText(text, this.stdout.columns - MARGINS.FOLDER_COLUMN_END, cutFrom); | ||
// This is necessary for the coloring of the text, since | ||
// the shortener takes into ansi-scape codes invisible | ||
// characters and can cause an error in the cli. | ||
text = this.paintStatusFolderPath(text, folder.status); | ||
return text; | ||
} | ||
paintStatusFolderPath(folderString, action) { | ||
const TRANSFORMATIONS = { | ||
// tslint:disable-next-line: object-literal-key-quotes | ||
deleted: (text) => text.replace(INFO_MSGS.DELETED_FOLDER, colors.green(INFO_MSGS.DELETED_FOLDER)), | ||
// tslint:disable-next-line: object-literal-key-quotes | ||
deleting: (text) => text.replace(INFO_MSGS.DELETING_FOLDER, colors.yellow(INFO_MSGS.DELETING_FOLDER)), | ||
'error-deleting': (text) => text.replace(INFO_MSGS.ERROR_DELETING_FOLDER, colors.red(INFO_MSGS.ERROR_DELETING_FOLDER)), | ||
}; | ||
return TRANSFORMATIONS[action] | ||
? TRANSFORMATIONS[action](folderString) | ||
: folderString; | ||
} | ||
clearFolderSection() { | ||
for (let row = MARGINS.ROW_RESULTS_START; row < this.stdout.rows; row++) { | ||
this.clearLine(row); | ||
} | ||
} | ||
setupEventsListener() { | ||
this.stdin.on('keypress', (ch, key) => { | ||
this.uiService.stdin.on('keypress', (_, key) => { | ||
if (key && key['name']) | ||
@@ -390,7 +235,12 @@ this.keyPress(key); | ||
this.stdout.on('resize', () => { | ||
this.clear(); | ||
this.printUI(); | ||
this.printStats(); | ||
this.printFoldersSection(); | ||
this.uiService.clear(); | ||
this.uiService.renderAll(); | ||
this.uiService.renderAll(); | ||
}); | ||
process.on('uncaughtException', (err) => { | ||
this.newError(err.message); | ||
}); | ||
process.on('unhandledRejection', (reason) => { | ||
this.newError(reason['stack']); | ||
}); | ||
} | ||
@@ -401,22 +251,8 @@ keyPress(key) { | ||
this.quit(); | ||
const command = this.getCommand(name); | ||
if (command) | ||
this.KEYS.execute(name); | ||
if (name !== 'space') | ||
this.printFoldersSection(); | ||
if (!this.activeComponent) { | ||
this.logger.error('activeComponent is NULL in Controller.'); | ||
return; | ||
} | ||
this.activeComponent.onKeyInput(key); | ||
} | ||
setErrorEvents() { | ||
process.on('uncaughtException', (err) => { | ||
this.printError(err.message); | ||
}); | ||
process.on('unhandledRejection', (reason) => { | ||
this.printError(reason['stack']); | ||
}); | ||
} | ||
hideCursor() { | ||
this.print(ansiEscapes.cursorHide); | ||
} | ||
showCursor() { | ||
this.print(ansiEscapes.cursorShow); | ||
} | ||
scan() { | ||
@@ -429,5 +265,6 @@ const params = this.prepareListDirParams(); | ||
const folders$ = this.fileService.listDir(params); | ||
this.logger.info(`Scan started in ${params.path}`); | ||
folders$ | ||
.pipe(catchError((error, caught) => { | ||
this.printFolderError(error.message); | ||
this.newError(error.message); | ||
return caught; | ||
@@ -444,8 +281,13 @@ }), mergeMap((dataFolder) => from(this.consoleService.splitData(dataFolder))), filter((path) => !!path), filter((path) => !isExcludedDangerousDirectory(path)), map((path) => { | ||
this.resultsService.addResult(nodeFolder); | ||
this.logger.info(`Folder found: ${nodeFolder.path}`); | ||
if (this.config.sortBy === 'path') { | ||
this.resultsService.sortResults(this.config.sortBy); | ||
this.clearFolderSection(); | ||
this.uiResults.clear(); | ||
} | ||
}), mergeMap((nodeFolder) => this.calculateFolderStats(nodeFolder), 2)) | ||
.subscribe(() => this.printFoldersSection(), (error) => this.printError(error), () => this.completeSearch()); | ||
.subscribe({ | ||
next: () => this.printFoldersSection(), | ||
error: (error) => this.newError(error), | ||
complete: () => this.completeSearch(), | ||
}); | ||
} | ||
@@ -463,10 +305,8 @@ prepareListDirParams() { | ||
} | ||
printFolderError(err) { | ||
if (!this.config.showErrors) | ||
return; | ||
const messages = this.consoleService.splitData(err); | ||
messages.map((msg) => this.printError(msg)); | ||
} | ||
calculateFolderStats(nodeFolder) { | ||
return this.fileService.getFolderSize(nodeFolder.path).pipe(tap((size) => (nodeFolder.size = this.transformFolderSize(size))), switchMap(async () => { | ||
this.logger.info(`Calculating stats for ${nodeFolder.path}`); | ||
return this.fileService.getFolderSize(nodeFolder.path).pipe(tap((size) => { | ||
nodeFolder.size = this.fileService.convertKbToGB(+size); | ||
this.logger.info(`Size of ${nodeFolder.path}: ${size}kb`); | ||
}), switchMap(async () => { | ||
// Saves resources by not scanning a result that is probably not of interest | ||
@@ -480,2 +320,3 @@ if (nodeFolder.isDangerous) { | ||
nodeFolder.modificationTime = result; | ||
this.logger.info(`Last mod. of ${nodeFolder.path}: ${result}`); | ||
}), tap(() => this.finishFolderStats())); | ||
@@ -487,17 +328,12 @@ } | ||
this.resultsService.sortResults(this.config.sortBy); | ||
this.clearFolderSection(); | ||
this.uiResults.clear(); | ||
} | ||
this.printStats(); | ||
this.uiStats.render(); | ||
this.printFoldersSection(); | ||
} | ||
transformFolderSize(size) { | ||
return this.fileService.convertKbToGB(+size); | ||
} | ||
completeSearch() { | ||
this.setSearchDuration(); | ||
this.finishSearching$.next(true); | ||
this.updateStatus(colors.green(INFO_MSGS.SEARCH_COMPLETED) + | ||
colors.gray(`${this.searchDuration}s`)); | ||
if (!this.resultsService.results.length) | ||
this.showNoResults(); | ||
this.uiResults.completeSearch(); | ||
this.uiStatus.completeSearch(this.searchDuration); | ||
this.logger.info(`Search completed after ${this.searchDuration}s`); | ||
} | ||
@@ -507,14 +343,13 @@ setSearchDuration() { | ||
} | ||
showNoResults() { | ||
this.resultsService.noResultsAfterCompleted = true; | ||
this.printNoResults(); | ||
} | ||
isQuitKey(ctrl, name) { | ||
return (ctrl && name === 'c') || name === 'q' || name === 'escape'; | ||
return (ctrl && name === 'c') || name === 'q'; | ||
} | ||
quit() { | ||
this.setRawMode(false); | ||
this.clear(); | ||
this.uiService.setRawMode(false); | ||
this.uiService.clear(); | ||
this.uiService.setCursorVisible(true); | ||
this.printExitMessage(); | ||
this.showCursor(); | ||
this.logger.info('Thank for using npkill. Bye!'); | ||
const logPath = this.logger.getSuggestLogfilePath(); | ||
this.logger.saveToFile(logPath); | ||
process.exit(); | ||
@@ -524,86 +359,11 @@ } | ||
const { spaceReleased } = this.resultsService.getStats(); | ||
const exitMessage = `Space released: ${spaceReleased}\n`; | ||
this.print(exitMessage); | ||
new GeneralUi().printExitMessage({ spaceReleased }); | ||
} | ||
getCommand(keyName) { | ||
return VALID_KEYS.find((name) => name === keyName); | ||
} | ||
isCursorInLowerTextLimit(positionY) { | ||
const foldersAmmount = this.resultsService.results.length; | ||
return positionY < foldersAmmount - 1 + MARGINS.ROW_RESULTS_START; | ||
} | ||
isCursorInUpperTextLimit(positionY) { | ||
return positionY > MARGINS.ROW_RESULTS_START; | ||
} | ||
moveCursorUp() { | ||
if (this.isCursorInUpperTextLimit(this.cursorPosY)) { | ||
this.previusCursorPosY = this.getRealCursorPosY(); | ||
this.cursorPosY--; | ||
this.fitScroll(); | ||
} | ||
} | ||
moveCursorDown() { | ||
if (this.isCursorInLowerTextLimit(this.cursorPosY)) { | ||
this.previusCursorPosY = this.getRealCursorPosY(); | ||
this.cursorPosY++; | ||
this.fitScroll(); | ||
} | ||
} | ||
moveCursorPageUp() { | ||
this.previusCursorPosY = this.getRealCursorPosY(); | ||
const resultsInPage = this.stdout.rows - MARGINS.ROW_RESULTS_START; | ||
this.cursorPosY -= resultsInPage - 1; | ||
if (this.cursorPosY - MARGINS.ROW_RESULTS_START < 0) | ||
this.cursorPosY = MARGINS.ROW_RESULTS_START; | ||
this.fitScroll(); | ||
} | ||
moveCursorPageDown() { | ||
this.previusCursorPosY = this.getRealCursorPosY(); | ||
const resultsInPage = this.stdout.rows - MARGINS.ROW_RESULTS_START; | ||
const foldersAmmount = this.resultsService.results.length; | ||
this.cursorPosY += resultsInPage - 1; | ||
if (this.cursorPosY - MARGINS.ROW_RESULTS_START > foldersAmmount) | ||
this.cursorPosY = foldersAmmount + MARGINS.ROW_RESULTS_START - 1; | ||
this.fitScroll(); | ||
} | ||
moveCursorFirstResult() { | ||
this.previusCursorPosY = this.getRealCursorPosY(); | ||
this.cursorPosY = MARGINS.ROW_RESULTS_START; | ||
this.fitScroll(); | ||
} | ||
moveCursorLastResult() { | ||
this.previusCursorPosY = this.getRealCursorPosY(); | ||
this.cursorPosY = | ||
MARGINS.ROW_RESULTS_START + this.resultsService.results.length - 1; | ||
this.fitScroll(); | ||
} | ||
fitScroll() { | ||
const shouldScrollUp = this.cursorPosY < MARGINS.ROW_RESULTS_START + this.scroll; | ||
const shouldScrollDown = this.cursorPosY > this.stdout.rows + this.scroll - 1; | ||
let scrollRequired = 0; | ||
if (shouldScrollUp) | ||
scrollRequired = | ||
this.cursorPosY - MARGINS.ROW_RESULTS_START - this.scroll; | ||
else if (shouldScrollDown) { | ||
scrollRequired = this.cursorPosY - this.stdout.rows - this.scroll + 1; | ||
} | ||
if (scrollRequired) | ||
this.scrollFolderResults(scrollRequired); | ||
} | ||
scrollFolderResults(scrollAmount) { | ||
const virtualFinalScroll = this.scroll + scrollAmount; | ||
this.scroll = this.clamp(virtualFinalScroll, 0, this.resultsService.results.length); | ||
this.clearFolderSection(); | ||
} | ||
delete() { | ||
const nodeFolder = this.resultsService.results[this.cursorPosY - MARGINS.ROW_RESULTS_START]; | ||
this.clearErrors(); | ||
this.deleteFolder(nodeFolder); | ||
} | ||
deleteFolder(folder) { | ||
const isSafeToDelete = this.fileService.isSafeToDelete(folder.path, this.config.targetFolder); | ||
if (!isSafeToDelete) { | ||
this.printError('Folder not safe to delete'); | ||
this.newError(`Folder not safe to delete: ${folder.path}`); | ||
return; | ||
} | ||
this.logger.info(`Deleting ${folder.path}`); | ||
folder.status = 'deleting'; | ||
@@ -615,4 +375,5 @@ this.printFoldersSection(); | ||
folder.status = 'deleted'; | ||
this.printStats(); | ||
this.uiStats.render(); | ||
this.printFoldersSection(); | ||
this.logger.info(`Deleted ${folder.path}`); | ||
}) | ||
@@ -622,45 +383,9 @@ .catch((e) => { | ||
this.printFoldersSection(); | ||
this.printError(e.message); | ||
this.newError(e.message); | ||
}); | ||
} | ||
printError(error) { | ||
const errorText = this.prepareErrorMsg(error); | ||
this.printAt(colors.red(errorText), { | ||
x: 0, | ||
y: this.stdout.rows, | ||
}); | ||
newError(error) { | ||
this.logger.error(error); | ||
this.uiStats.render(); | ||
} | ||
prepareErrorMsg(errMessage) { | ||
const margin = MARGINS.FOLDER_COLUMN_START; | ||
const width = this.stdout.columns - margin - 3; | ||
return this.consoleService.shortenText(errMessage, width, width); | ||
} | ||
printStats() { | ||
const { totalSpace, spaceReleased } = this.resultsService.getStats(); | ||
const totalSpacePosition = { ...UI_POSITIONS.TOTAL_SPACE }; | ||
const spaceReleasedPosition = { ...UI_POSITIONS.SPACE_RELEASED }; | ||
totalSpacePosition.x += INFO_MSGS.TOTAL_SPACE.length; | ||
spaceReleasedPosition.x += INFO_MSGS.SPACE_RELEASED.length; | ||
this.printAt(totalSpace, totalSpacePosition); | ||
this.printAt(spaceReleased, spaceReleasedPosition); | ||
} | ||
getVisibleScrollFolders() { | ||
return this.resultsService.results.slice(this.scroll, this.stdout.rows - MARGINS.ROW_RESULTS_START + this.scroll); | ||
} | ||
getRealCursorPosY() { | ||
return this.cursorPosY - this.scroll; | ||
} | ||
clearErrors() { | ||
const lineOfErrors = this.stdout.rows; | ||
this.clearLine(lineOfErrors); | ||
} | ||
clearLine(row) { | ||
this.printAt(ansiEscapes.eraseLine, { x: 0, y: row }); | ||
} | ||
getUserHomePath() { | ||
return homedir(); | ||
} | ||
clamp(num, min, max) { | ||
return Math.min(Math.max(num, min), max); | ||
} | ||
} |
@@ -13,3 +13,3 @@ import { Observable, of } from 'rxjs'; | ||
return function (source$) { | ||
let buffer = new Buffer(); | ||
const buffer = new Buffer(); | ||
return new Observable((observer) => { | ||
@@ -16,0 +16,0 @@ const resetNotifierSubscription = resetNotifier.subscribe(() => buffer.reset()); |
import { ConsoleService, HttpsService, LinuxFilesService, MacFilesService, ResultsService, SpinnerService, StreamService, UpdateService, WindowsFilesService, } from './services/index.js'; | ||
import { Controller } from './controller.js'; | ||
import { FileWorkerService } from './services/files/files.worker.service.js'; | ||
import { UiService } from './services/ui.service.js'; | ||
import { LoggerService } from './services/logger.service.js'; | ||
const getOS = () => process.platform; | ||
@@ -10,6 +12,7 @@ const OSService = { | ||
}; | ||
const logger = new LoggerService(); | ||
const fileWorkerService = new FileWorkerService(); | ||
const streamService = new StreamService(); | ||
const fileService = new OSService[getOS()](streamService, fileWorkerService); | ||
export const controller = new Controller(fileService, new SpinnerService(), new ConsoleService(), new UpdateService(new HttpsService()), new ResultsService()); | ||
export const controller = new Controller(logger, fileService, new SpinnerService(), new ConsoleService(), new UpdateService(new HttpsService()), new ResultsService(), new UiService()); | ||
export default () => controller.init(); |
@@ -0,3 +1,4 @@ | ||
import { OPTIONS, WIDTH_OVERFLOW } from '../constants/index.js'; | ||
import { extname } from 'path'; | ||
import { OPTIONS, WIDTH_OVERFLOW } from '../constants/index.js'; | ||
import * as readline from 'node:readline'; | ||
export class ConsoleService { | ||
@@ -28,4 +29,4 @@ getParameters(rawArgv) { | ||
} | ||
replaceString(string, stringToReplace, replaceValue) { | ||
return string.replace(stringToReplace, replaceValue); | ||
replaceString(text, textToReplace, replaceValue) { | ||
return text.replace(textToReplace, replaceValue); | ||
} | ||
@@ -43,2 +44,5 @@ shortenText(text, width, startCut = 0) { | ||
} | ||
startListenKeyEvents() { | ||
readline.emitKeypressEvents(process.stdin); | ||
} | ||
/** Argvs can be specified for example by | ||
@@ -45,0 +49,0 @@ * "--sort size" and "--sort=size". The main function |
@@ -43,18 +43,15 @@ import { readFileSync } from 'fs'; | ||
for (const item of items) { | ||
try { | ||
if (item.isDirectory()) { | ||
if (item.name === 'node_modules') | ||
continue; | ||
files = [ | ||
...files, | ||
...(await this.getFileStatsInDir(`${dirname}/${item.name}`)), | ||
]; | ||
} | ||
else { | ||
const path = `${dirname}/${item.name}`; | ||
const fileStat = await stat(path); | ||
files.push({ path, modificationTime: fileStat.mtimeMs / 1000 }); | ||
} | ||
if (item.isDirectory()) { | ||
if (item.name === 'node_modules') | ||
continue; | ||
files = [ | ||
...files, | ||
...(await this.getFileStatsInDir(`${dirname}/${item.name}`)), | ||
]; | ||
} | ||
catch (error) { } | ||
else { | ||
const path = `${dirname}/${item.name}`; | ||
const fileStat = await stat(path); | ||
files.push({ path, modificationTime: fileStat.mtimeMs / 1000 }); | ||
} | ||
} | ||
@@ -61,0 +58,0 @@ return files; |
'use strict'; | ||
import { opendir } from 'fs'; | ||
import EventEmitter from 'events'; | ||
import { memoryUsage } from 'process'; | ||
import { parentPort } from 'worker_threads'; | ||
import EventEmitter from 'events'; | ||
var ETaskOperation; | ||
@@ -130,3 +130,3 @@ (function (ETaskOperation) { | ||
while (this.procs < this.MAX_PROCS && this.taskQueue.length > 0) { | ||
let path = this.taskQueue.shift().path; | ||
const path = this.taskQueue.shift().path; | ||
this.run(path); | ||
@@ -133,0 +133,0 @@ } |
@@ -0,7 +1,6 @@ | ||
import { dirname, extname } from 'path'; | ||
import { Worker } from 'worker_threads'; | ||
import { dirname, extname } from 'path'; | ||
export class FileWorkerService { | ||
scanWorker = new Worker(this.getWorkerPath()); | ||
getSizeWorker = new Worker(this.getWorkerPath()); | ||
constructor() { } | ||
startScan(stream$, params) { | ||
@@ -21,7 +20,6 @@ this.scanWorker.postMessage({ | ||
this.scanWorker.on('error', (error) => { | ||
console.log('this.worker error', error); | ||
this.scanWorker.terminate(); | ||
throw error; | ||
}); | ||
this.scanWorker.on('exit', (code) => { | ||
console.log('this.worker Exit'); | ||
if (code !== 0) { | ||
@@ -28,0 +26,0 @@ this.scanWorker.terminate(); |
import { FOLDER_SORT } from '../constants/sort.result.js'; | ||
export class ResultsService { | ||
results = []; | ||
noResultsAfterCompleted = false; | ||
addResult(result) { | ||
@@ -6,0 +5,0 @@ this.results = [...this.results, result]; |
{ | ||
"name": "npkill", | ||
"version": "0.10.1-1", | ||
"version": "0.11.0-0", | ||
"description": "List any node_modules directories in your system, as well as the space they take up. You can then select which ones you want to erase to free up space.", | ||
@@ -37,3 +37,2 @@ "exports": "./lib/index.js", | ||
], | ||
"ethereum": "0x7668e86c8bdb52034606db5aa0d2d4d73a0d4259", | ||
"scripts": { | ||
@@ -55,3 +54,2 @@ "build": "gulp", | ||
"get-folder-size": "^2.0.0", | ||
"keypress": "^0.2.1", | ||
"node-emoji": "^1.10.0", | ||
@@ -101,3 +99,4 @@ "rxjs": "^7.5.7" | ||
] | ||
} | ||
}, | ||
"ethereum": "0x7668e86c8bdb52034606db5aa0d2d4d73a0d4259" | ||
} |
@@ -96,3 +96,3 @@ <p align="center"> | ||
| -D, --delete-all | CURRENTLY DISABLED. Automatically delete all node_modules folders that are found | | ||
| -e, --show-errors | Show error messages related to the search if any | | ||
| -e, --hide-errors | Hide errors if any | | ||
| -E, --exclude | Exclude directories from search (directory list must be inside double quotes "", each directory separated by ',' ) Example: "ignore1, ignore2" | | ||
@@ -190,4 +190,4 @@ | -f, --full | Start searching from the home of the user (example: "/home/user" in linux) | | ||
- [ ] Create option for displaying directories in tree format | ||
- [ ] Add some menus | ||
- [ ] Add log service | ||
- [x] Add some menus | ||
- [x] Add log service | ||
- [ ] Periodic and automatic cleaning (?) | ||
@@ -194,0 +194,0 @@ |
90491
5
61
2213
7
- Removedkeypress@^0.2.1
- Removedkeypress@0.2.1(transitive)