Skip to content
Open
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
1 change: 1 addition & 0 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const cli = meow(`
Run without arguments to use the interactive mode.
In interactive mode, 🚦n% indicates high CPU usage and 🐏n% indicates high memory usage.
Supports fuzzy search in the interactive mode.
Same-name processes are grouped (for example, "Google Chrome (12)"). Select a group to kill all or pick individuals.
The process name is case-insensitive by default.
`, {
Expand Down
17 changes: 17 additions & 0 deletions group-by-name.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const groupByName = processes => {
const groups = new Map();

for (const process_ of processes) {
const existing = groups.get(process_.name);

if (existing) {
existing.push(process_);
} else {
groups.set(process_.name, [process_]);
}
}

return [...groups.values()];
};

export default groupByName;
101 changes: 97 additions & 4 deletions interactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {allPortsWithPid} from 'pid-port';
import fkill from 'fkill';
import {processExists} from 'process-exists';
import FuzzySearch from 'fuzzy-search';
import groupByName from './group-by-name.js';

const isWindows = process.platform === 'win32';
const commandLineMargins = 4;
Expand Down Expand Up @@ -117,6 +118,46 @@ const renderProcessForDisplay = (process_, flags, memoryThreshold, cpuThreshold)
};
};

const renderProcessGroupForDisplay = groupedProcesses => {
const {name} = groupedProcesses[0];
return {
name: `${name} (${groupedProcesses.length})`,
value: groupedProcesses,
};
};

const renderIndividualProcessChoice = (process_, flags) => {
const pid = String(process_.pid);

if (!flags.verbose || isWindows || !process_.cmd) {
return pid;
}

const lineLength = process.stdout.columns || 80;
const cmd = cliTruncate(process_.cmd, Math.max(lineLength - pid.length - 1, 1), {
position: 'middle',
preferTruncationOnSpace: true,
});

return `${pid} ${cmd}`;
};

const shouldGroupSearchResults = term => !term.startsWith(':');

const createChooserChoices = (processes, flags, {memoryThreshold, cpuThreshold, group = true} = {}) => {
if (!group) {
return processes.map(process_ => renderProcessForDisplay(process_, flags, memoryThreshold, cpuThreshold));
}

return groupByName(processes).map(groupedProcesses => {
if (groupedProcesses.length === 1) {
return renderProcessForDisplay(groupedProcesses[0], flags, memoryThreshold, cpuThreshold);
}

return renderProcessGroupForDisplay(groupedProcesses);
});
};

const searchProcessesByPort = (processes, port) => processes.filter(process_ => process_.ports.includes(port));

const searchProcessByPid = (processes, pid) => processes.find(process_ => String(process_.pid) === pid);
Expand Down Expand Up @@ -251,21 +292,68 @@ const findPortsForProcess = (processId, portToPidMap) => {
return ports;
};

const promptGroupedProcesses = async (processes, flags) => {
const {name} = processes[0];
const {action} = await inquirer.prompt([{
type: 'list',
name: 'action',
message: `${name} (${processes.length})`,
choices: [
{
name: 'Kill all',
value: 'all',
},
{
name: 'Pick individuals',
value: 'pick',
},
],
}]);

if (action === 'all') {
await performKillSequence(processes.map(process_ => process_.pid));
return;
}

const {pids} = await inquirer.prompt([{
type: 'checkbox',
name: 'pids',
message: 'Select processes to kill:',
choices: processes.map(process_ => ({
name: renderIndividualProcessChoice(process_, flags),
value: process_.pid,
})),
}]);

if (pids.length > 0) {
await performKillSequence(pids);
}
};

const listProcesses = async (processes, flags) => {
const memoryThreshold = flags.verbose ? 0 : 1;
const cpuThreshold = flags.verbose ? 0 : 3;
const searcher = new FuzzySearch(processes, ['name'], {caseSensitive: false});

const selectedPid = await search({
const selected = await search({
message: 'Running processes:',
pageSize: 10,
async source(term = '') {
const matchingProcesses = filterAndSortProcesses(processes, term, searcher, flags);
return matchingProcesses.map(process_ => renderProcessForDisplay(process_, flags, memoryThreshold, cpuThreshold));
return createChooserChoices(matchingProcesses, flags, {
memoryThreshold,
cpuThreshold,
group: shouldGroupSearchResults(term),
});
},
});

performKillSequence(selectedPid);
if (Array.isArray(selected)) {
await promptGroupedProcesses(selected, flags);
return;
}

await performKillSequence(selected);
};

const init = async flags => {
Expand All @@ -284,4 +372,9 @@ const init = async flags => {
listProcesses(processesWithPorts, flags);
};

export {init, handleFkillError};
export {
init,
handleFkillError,
createChooserChoices,
shouldGroupSearchResults,
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
},
"files": [
"cli.js",
"interactive.js"
"interactive.js",
"group-by-name.js"
],
"keywords": [
"cli-app",
Expand Down
3 changes: 3 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ $ fkill --help
Run without arguments to use the interactive interface.
In interactive mode, 🚦n% indicates high CPU usage and 🐏n% indicates high memory usage.
Supports fuzzy search in the interactive mode.
Same-name processes are grouped (for example, "Google Chrome (12)"). Select a group to kill all or pick individuals.

The process name is case-insensitive by default.
```
Expand All @@ -52,6 +53,8 @@ $ fkill --help

Run `fkill` without arguments to launch the interactive UI.

Processes with the same name are grouped into one row, for example `Google Chrome (12)`. Selecting a group lets you kill all of them or pick individuals. Port (`:8080`) and PID searches stay ungrouped.

![](screenshot.svg)

## Related
Expand Down
75 changes: 75 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import delay from 'delay';
import noopProcess from 'noop-process';
import {processExists} from 'process-exists';
import getPort from 'get-port';
import groupByName from './group-by-name.js';
import {createChooserChoices, shouldGroupSearchResults} from './interactive.js';

const noopProcessKilled = async (t, pid) => {
// Ensure the noop process has time to exit
Expand Down Expand Up @@ -97,3 +99,76 @@ test('silent flag with -s shortflag works', async t => {
const {exitCode} = await execa('./cli.js', ['-s', '--force', ':1337']);
t.is(exitCode, 0);
});

const chromeOne = {
name: 'Google Chrome',
cmd: 'Google Chrome --type=renderer',
pid: 101,
ports: [],
cpu: 0,
memory: 0,
};

const chromeTwo = {
name: 'Google Chrome',
cmd: 'Google Chrome --type=gpu-process',
pid: 102,
ports: [],
cpu: 0,
memory: 0,
};

const redis = {
name: 'redis',
cmd: 'redis-server',
pid: 201,
ports: ['6379'],
cpu: 0,
memory: 0,
};

test('groupByName groups same-name processes and keeps first-seen order', t => {
const groups = groupByName([chromeOne, redis, chromeTwo]);

t.is(groups.length, 2);
t.deepEqual(groups[0], [chromeOne, chromeTwo]);
t.deepEqual(groups[1], [redis]);
});

test('groupByName returns empty array for no processes', t => {
t.deepEqual(groupByName([]), []);
});

test('groupByName keeps singleton names as single-item groups', t => {
t.deepEqual(groupByName([redis]), [[redis]]);
});

test('createChooserChoices collapses duplicate names and leaves singletons unchanged', t => {
const choices = createChooserChoices([chromeOne, chromeTwo, redis], {}, {
memoryThreshold: 1,
cpuThreshold: 3,
});

t.is(choices.length, 2);
t.is(choices[0].name, 'Google Chrome (2)');
t.deepEqual(choices[0].value, [chromeOne, chromeTwo]);
t.is(choices[1].value, 201);
});

test('createChooserChoices can leave matching processes ungrouped', t => {
const choices = createChooserChoices([chromeOne, chromeTwo], {}, {
memoryThreshold: 1,
cpuThreshold: 3,
group: false,
});

t.is(choices.length, 2);
t.is(choices[0].value, 101);
t.is(choices[1].value, 102);
});

test('port searches stay ungrouped; name searches are grouped', t => {
t.false(shouldGroupSearchResults(':8080'));
t.true(shouldGroupSearchResults(''));
t.true(shouldGroupSearchResults('chrome'));
});